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
43 changes: 28 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Verifies lint, formatting, types, audit, and build all pass.
# Builds the CLI and compiles it into executables for each platform for a smoke test.
name: build
on:
workflow_call:
Expand All @@ -8,28 +8,41 @@ on:
type: string

jobs:
check:
runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}
build:
name: Build (${{ matrix.name }})
runs-on: ${{ fromJSON(matrix.runner) }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- name: Linux
runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]'
target: linux-x64
binary: ./dist/bin/agentcore-linux-x64
- name: Windows
runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]'
target: windows-x64
binary: ./dist/bin/agentcore-windows-x64.exe
# CodeBuild does not support macOS.
- name: macOS
runner: '["macos-latest"]'
target: darwin-arm64
binary: ./dist/bin/agentcore-darwin-arm64
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run lint:check
if: always()
- run: bun run format:check
if: always()
- run: bun run typecheck
if: always()
- run: bun audit
if: always()

- run: bun run build
if: always()

- run: bun pm pack
if: always()
- run: bun run compile
if: always()

- run: bun run compile:${{ matrix.target }}

- name: Smoke test binary
run: ${{ matrix.binary }} --help
29 changes: 29 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Verifies lint, formatting, types, audit, and other static checks pass.
name: check
on:
workflow_call:
inputs:
ref:
required: true
type: string

jobs:
check:
runs-on: codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run lint:check
if: always()
- run: bun run format:check
if: always()
- run: bun run typecheck
if: always()
- run: bun audit
if: always()
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ concurrency:
cancel-in-progress: true

jobs:
check:
uses: ./.github/workflows/check.yml
permissions:
contents: read
with:
ref: ${{ github.event.pull_request.head_sha || github.sha }}
build:
uses: ./.github/workflows/build.yml
permissions:
Expand Down
86 changes: 59 additions & 27 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
"react": "^19.2.7",
"react-devtools-core": "^7.0.1",
"react-router": "^8.1.0",
"pino": "^10.3.1",
"pino-roll": "^4.0.0",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
"zod": "^4.4.3"
}
}
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ process.exit(
await rootHandler.route(argv);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
io.stderr.write(`${error.name}: ${error.message}\n`);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to top-level to ensure that logger/io init failures are logged to console.

rootLogger
.child({ errorName: error.name, errorMessage: error.message, stack: error.stack ?? "" })
.error();
throw e;
} finally {
await rootLogger.flush();
await rootLogger.end();
}
}),
);
77 changes: 43 additions & 34 deletions src/logging/fileLogger.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pino from "pino";
import winston from "winston";
import DailyRotateFile from "winston-daily-rotate-file";
import { type AsyncLogger, type LoggerBindings, type LogLevel } from "./types";

export interface FileLoggerConfig {
Expand All @@ -9,54 +10,62 @@ export interface FileLoggerConfig {
logLevel: LogLevel;
}

function wrapPinoLogger(pinoLogger: pino.Logger): AsyncLogger {
function wrapWinstonLogger(
winstonLogger: winston.Logger,
transport: DailyRotateFile,
bindings: LoggerBindings,
): AsyncLogger {
const log =
(level: pino.Level) =>
(level: string) =>
(...args: string[]) =>
pinoLogger[level](args.join(" "));
winstonLogger.log(level, args.join(" "), bindings);

return {
debug: log("debug"),
info: log("info"),
warn: log("warn"),
error: log("error"),
child: (bindings) => wrapPinoLogger(pinoLogger.child(bindings)),
// we convert pino's flush method that accepts a callback into a promise to make it easier to work with.
// Note: we also treat flush as best-effort and swallow errors
flush: () => new Promise<void>((resolve) => pinoLogger.flush(() => resolve())),
child: (childBindings) =>
wrapWinstonLogger(winstonLogger, transport, { ...bindings, ...childBindings }),
end: () =>
new Promise<void>((resolve) => {
transport.on("finish", resolve);
// note: we prefer close over end since close calls end on the stream internally: https://github.com/winstonjs/winston-daily-rotate-file/blob/a1a4668cfea77476cd6a4a11f038c2aac9d10741/daily-rotate-file.js#L201-L207
if (transport.close) transport.close();
}),
};
}

/**
* Creates a logger that writes structured JSON to a rotating file.
*
* @param config - Logger configuration (file path, rotation limits, level).
* @returns A {@link AsyncLogger} that writes to a rotating file via pino.
* @returns A {@link AsyncLogger} that writes to a rotating file via winston.
*/
export function createFileLogger(config: FileLoggerConfig): AsyncLogger {
const maxSizeInMB = config.maxSizeInMB ?? 10;
const maxFileCount = config.maxFileCount ?? 5;
const maxSizeInMB = config.maxSizeInMB ?? 5;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new behavior is that it rotates the file each day or when the size cap it hit. Before we rotated only when the size limit was hit. This swap ensures we maintain roughly the past 10 days but still no more than 50MB.

const maxFileCount = config.maxFileCount ?? 10;
const bindings = config.bindings ?? {};
return wrapPinoLogger(
pino({
level: config.logLevel,
base: undefined, // omit pid and hostname
formatters: {
level(label) {
return { level: label };
},
},
transport: {
target: "pino-roll",
options: {
extension: ".log",
dateFormat: "yyyy-MM-dd'T'HH-mm-ss",
// Rotate when file reaches {maxSizeInMB} MB, and start deleting once we have {maxFileCount} files
size: `${maxSizeInMB}m`,
limit: { count: maxFileCount },
file: config.filePath,
mkdir: true,
},
},
}),
).child(bindings);

const transport = new DailyRotateFile({
filename: `${config.filePath}-%DATE%`,
extension: ".log",
datePattern: "YYYY-MM-DD",
maxSize: `${maxSizeInMB}m`,
maxFiles: maxFileCount,
createSymlink: false,
});

const jsonFormat = winston.format.printf((info) => {
const { level, message, ...rest } = info;
return JSON.stringify({ level, msg: message, time: Date.now(), ...rest });
});

const logger = winston.createLogger({
level: config.logLevel,
format: jsonFormat,
transports: [transport],
});

return wrapWinstonLogger(logger, transport, bindings);
}
3 changes: 2 additions & 1 deletion src/logging/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ export interface Logger {
/** An extension of {@link Logger} that writes logs asynchronously and requires output to be flushed */
export interface AsyncLogger extends Logger {
child: (bindings: LoggerBindings) => AsyncLogger;
flush: () => Promise<void>;
/** Flushes the pending logs and closes the underlying logging streams **/
end: () => Promise<void>;
}
2 changes: 1 addition & 1 deletion src/middleware/withLogging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("withLogging", () => {
});

afterEach(async () => {
await logger.flush();
await logger.end();
await rm(tempDir, { recursive: true, force: true });
});

Expand Down
4 changes: 3 additions & 1 deletion src/runnable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export async function runWithExitCode(
try {
await fn(argv);
return ExitCode.SUCCESS;
} catch {
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
console.error(`${error.name}: ${error.message}`);
return ExitCode.FAILURE;
}
}
3 changes: 1 addition & 2 deletions src/testing/logging.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export async function assertLogsMatch(
): Promise<void> {
let lastResults: ReturnType<typeof evaluateQueries> = [];

// pino-roll writes via async worker threads, so logs may not be flushed to
// disk immediately. Poll until all query conditions are satisfied.
// Poll until all query conditions are satisfied.
try {
await waitFor(async () => {
const content = await readLogFile(dir);
Expand Down
Loading