Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
70686f3
WIP migration of actions from separate page to be within workspace
AaronPlave Feb 26, 2026
da7db87
Tweaks
AaronPlave Feb 26, 2026
e47759f
Updated action demo
AaronPlave Feb 26, 2026
1b9842f
Fixes and removal of unused files
AaronPlave Feb 26, 2026
d96e9a2
Tweak
AaronPlave Feb 26, 2026
38995dd
Column size fixes
AaronPlave Feb 27, 2026
d07a60f
WIP
AaronPlave Mar 5, 2026
ba8e47f
WIP updates
AaronPlave Mar 5, 2026
7d47032
Tweaks
AaronPlave Mar 16, 2026
da196d5
More updates
AaronPlave Mar 16, 2026
8f722bd
Test fixes
AaronPlave Mar 17, 2026
c8817da
Format
AaronPlave Mar 17, 2026
54709c6
Styling
AaronPlave Mar 17, 2026
4c286b5
Format
AaronPlave Mar 24, 2026
fede7ce
Change actions error console run link to route through onViewActionRu…
AaronPlave Mar 25, 2026
a104634
Fixes and refactoring
AaronPlave Mar 26, 2026
d64e3ab
Permissions fix
AaronPlave Mar 26, 2026
568f721
Test fix
AaronPlave Mar 26, 2026
d7c8d77
Style tweak
AaronPlave Mar 30, 2026
b702b98
Test refactor
AaronPlave Mar 30, 2026
dd07d48
Fix status display
AaronPlave Mar 30, 2026
54298f0
Fix demo action
AaronPlave Mar 30, 2026
a421757
Reorder
AaronPlave Mar 30, 2026
e47fd6e
Refactor ConsoleLog to be more generic by using message slot. Refacto…
AaronPlave Mar 30, 2026
06eae2c
Show message when no parameters are found when running an action
AaronPlave Mar 30, 2026
40dcba8
Action run console log object formatting fix
AaronPlave Mar 30, 2026
bcef80e
Bug fixes, prevent user from archiving last action version, prevent u…
AaronPlave Mar 30, 2026
7a41186
Format
AaronPlave Mar 30, 2026
a05e52d
Add missing let declarations
AaronPlave Apr 1, 2026
018c1bd
Permissions fix for cancel
AaronPlave Apr 1, 2026
634153d
Rename variable
AaronPlave Apr 1, 2026
90b2bf4
Fix
AaronPlave Apr 1, 2026
1a41271
Refactor
AaronPlave Apr 1, 2026
550ff5c
Refactor
AaronPlave Apr 1, 2026
490ec4d
Fix
AaronPlave Apr 1, 2026
4fe8421
Merge branch 'develop' into feat/actions-page-rework
duranb Apr 8, 2026
b18ac05
test: update repository URL in e2e action demo to be consistent with …
duranb Apr 8, 2026
ac3e92f
UI changes: preserve whitespace in action logs, remove long timestamp…
dandelany Apr 14, 2026
0fd6f09
fix linting
dandelany Apr 14, 2026
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
1 change: 1 addition & 0 deletions docker-compose-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
depends_on: ['postgres']
environment:
HASURA_GRAPHQL_ADMIN_SECRET: '${HASURA_GRAPHQL_ADMIN_SECRET}'
HASURA_GRAPHQL_JWT_SECRET: '${HASURA_GRAPHQL_JWT_SECRET}'
LOG_FILE: console
LOG_LEVEL: debug
MERLIN_GRAPHQL_URL: http://hasura:8080/v1/graphql
Expand Down
348 changes: 299 additions & 49 deletions e2e-tests/data/aerie-action-demo.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,318 @@
'use strict';

// Define schemas for your action's settings and parameters
const parameterDefinitions = {
boolean: { type: "boolean" },
delay: { type:"int" },
duration: { type: "duration" },
file: { type: "file" },
fileList: { type: "fileList" },
real: { type: "real" },
repository: { type: "string" },
series: { items:{ type: "string" }, type: "series", },
string: { type: "string" },
sequence: { type: "sequence" },
sequenceList: { type: "sequenceList" },
variant: { type: "variant", variants: [{key: "foo", label: "Foo"}, {key: "bar", label: "Bar"}] },
logCount: {
type: 'int',
description: 'Number of log lines to generate (0 for none)',
defaultValue: 10,
},
mode: {
type: 'variant',
description: 'What the action should do',
variants: [
{ key: 'fetch', label: 'Fetch URL' },
{ key: 'adaptation', label: 'Translate File using Adaptation' },
{ key: 'files', label: 'List & Read Files' },
{ key: 'write', label: 'Write a File' },
{ key: 'error', label: 'Throw an Error' },
{ key: 'slow', label: 'Slow (5s delay)' },
],
},
outputFile: {
type: 'string',
description: 'Filename to write (used in "write" mode)',
defaultValue: 'action_output.txt',
},
outputContent: {
type: 'string',
description: 'Content to write to the file (used in "write" mode)',
defaultValue: 'Hello from aerie-action-demo!',
},
secret: {
type: 'secret',
description: 'A secret value (e.g. API token) — sent securely, never stored in run history',
},
sequence: {
type: 'sequence',
description: 'A sequence file parameter (for testing file pickers)',
primary: true,
},
};

const settingDefinitions = {
externalUrl: { type: "string" },
retries: { type: "int" }
externalUrl: {
type: 'string',
description: 'Base URL for fetch mode',
defaultValue: 'https://api.github.com',
},
secretSetting: {
type: 'secret',
description: 'A secret value (e.g. API token) — sent securely, never stored in run history',
},
files: {
type: 'fileList',
description: 'A list of files to process',
},
verbose: {
type: 'string',
description: 'Enable extra-verbose logging',
defaultValue: "false",
},
};

async function main(actionParameters, actionSettings, actionsAPI) {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, actionParameters.delay ?? 0);
})

const url = `${actionSettings.externalUrl}/${actionParameters.repository}`;
const startTime = performance.now();
// Make a request to an external URL using fetch
const result = await fetch(url, {
method: "get",
headers: {
"Content-Type": "application/json",
},
const { logCount = 10, mode = 'fetch', outputFile, outputContent, secret, sequence } = actionParameters;
const { externalUrl = 'https://api.github.com', verbose = false } = actionSettings;
const startTime = performance.now();

// Generate requested log output
emitLogs(logCount, verbose);

console.log(`Action started — mode: ${mode}`);
console.log(`Secret provided: ${secret ? 'yes (' + secret.length + ' chars)' : 'no'}`);
console.log(`Parameters: ${JSON.stringify({ ...actionParameters, secret: secret ? '***' : undefined })}`);

let result;
switch (mode) {
case 'fetch':
result = await runFetchMode(externalUrl, verbose);
break;
case 'files':
result = await runFilesMode(actionsAPI, sequence, verbose);
break;
case 'adaptation':
result = await runAdaptationMode(actionsAPI, sequence);
break;
case 'write':
result = await runWriteMode(actionsAPI, outputFile, outputContent, verbose);
break;
case 'error':
result = runErrorMode();
break;
case 'slow':
result = await runSlowMode(verbose);
break;
default:
result = { status: 'FAILED', data: { error: `Unknown mode: ${mode}` } };
}

const elapsed = (performance.now() - startTime).toFixed(1);
console.log(`Action completed in ${elapsed}ms with status: ${result.status}`);

return result;
}

function emitLogs(count, verbose) {
if (count <= 0) return;

const messages = [
'Initializing action runtime...',
'Loading workspace configuration...',
'Validating parameter schemas...',
'Resolving file dependencies...',
'Checking network connectivity...',
'Authenticating with external service...',
'Preparing execution context...',
'Allocating resources...',
'Starting main execution loop...',
'Processing batch 1 of N...',
'Intermediate checkpoint reached.',
'Cache hit for resource lookup.',
'Cache miss — fetching from remote.',
'Retrying transient operation (attempt 2/3)...',
'Rate limit approaching — throttling requests.',
'Partial result received, continuing...',
'Unexpected response format — attempting fallback parse.',
'File handle released.',
'Connection pool drained to 1 active.',
'Garbage collection triggered.',
'Serializing intermediate results...',
'Compressing output payload...',
'Flushing write buffer...',
'Waiting for async callbacks...',
'Finalizing transaction log...',
];

for (let i = 0; i < count; i++) {
const msg = messages[i % messages.length];
if (verbose) {
const ts = new Date().toISOString();
console.log(`[${ts}] [line ${i + 1}/${count}] ${msg}`);
} else {
console.log(msg);
}
}
}

async function runFetchMode(externalUrl, verbose) {
const url = `${externalUrl}/repos/NASA-AMMOS/aerie`;
console.log(`Fetching: ${url}`);

try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
console.log(`request took ${performance.now() - startTime}ms`);
// try parsing result as either json or text
let resultData;

if (verbose) {
console.log(`Response status: ${response.status} ${response.statusText}`);
}

let data;
try {
resultData = await result.clone().json();
data = await response.clone().json();
} catch {
data = await response.clone().text();
}

if (!response.ok) {
console.warn(`Non-OK response: ${response.status}`);
return { status: 'FAILED', data };
}

// Return a curated subset so results aren't enormous
const summary = {
description: data.description,
forks: data.forks_count,
language: data.language,
name: data.full_name,
open_issues: data.open_issues_count,
stars: data.stargazers_count,
updated_at: data.updated_at,
url: data.html_url,
};

return { status: 'SUCCESS', data: summary };
} catch (err) {
console.error(`Fetch failed: ${err.message}`);
return { status: 'FAILED', data: { error: err.message } };
}
}

async function runFilesMode(actionsAPI, sequence, verbose) {
console.log('Listing workspace files...');
try {
// For now treat files as a JSON string that needs parsing. This should be fixed in the API to return an array directly.
const filesString = await actionsAPI.listFiles('.');
const files = JSON.parse(filesString);
console.log(`Found ${files.length} files`);

if (verbose) {
files.forEach((f, i) => console.log(` [${i}] ${f}`));
}
catch {
resultData = await result.clone().text();

let sequenceContent = null;
if (sequence) {
console.log(`Reading sequence file: ${sequence}`);
try {
sequenceContent = await actionsAPI.readFile(sequence);
console.log(`Sequence file read successfully (${sequenceContent.length} chars)`);
} catch (err) {
console.warn(`Could not read sequence file: ${err.message}`);
}
}
try {
// read/write files using the actions helpers
const files = await actionsAPI.listSequences();
const myFile = await actionsAPI.readSequence("my_file");
const writeResult = await actionsAPI.writeSequence("new_file", "new contents");
console.log(`writeResult: ${JSON.stringify(writeResult)}`);
console.log('sequence files:', JSON.stringify(files));
console.log(`myFile: ${JSON.stringify(myFile)}`);
} catch (error) {
console.log(error)

return {
status: 'SUCCESS',
data: {
fileCount: files.length,
files: files.slice(0, 20),
sequenceContent: sequenceContent ? sequenceContent.substring(0, 500) : null,
sequenceFile: sequence || null,
},
};
} catch (err) {
console.error(`File listing failed: ${err.message}`);
return { status: 'FAILED', data: { error: err.message } };
}
}

async function runWriteMode(actionsAPI, filename, content, verbose) {
const name = filename || 'action_output.txt';
const body = content || 'Written by aerie-action-demo';

console.log(`Writing file: ${name}`);
if (verbose) {
console.log(`Content length: ${body.length} chars`);
}

try {
const result = await actionsAPI.writeFile(name, body);
console.log('File written successfully');
return {
status: 'SUCCESS',
data: { filename: name, contentLength: body.length, writeResult: result },
};
} catch (err) {
console.error(`Write failed: ${err.message}`);
return { status: 'FAILED', data: { error: err.message } };
}
}


async function runAdaptationMode(actionsAPI, sequence) {
console.log('Loading adaptation');

try {
const adaptation = await actionsAPI.loadAdaptation();
console.log('Adaptation loaded');

if (!sequence) {
throw new Error('No sequence file provided for adaptation mode');
}

console.log(`Reading sequence file: ${sequence}`);
const sequenceContent = await actionsAPI.readFile(sequence);
console.log(`Sequence file read successfully (${sequenceContent.length} chars)`);
const translated = adaptation.outputs[0].toOutputFormat(
sequenceContent,
{ commandDictionary: null, channelDictionary: null, parameterDictionaries: [], librarySequences: [] },
'my-sequence'
);
console.log(`Sequence translated successfully (${translated.length} chars)`);

return {
status: "SUCCESS",
data: resultData,
status: 'SUCCESS',
data: { translated: JSON.parse(translated) },
};
} catch (err) {
console.error(`Translate sequence with adaptation failed: ${err.message}`);
return { status: 'FAILED', data: { error: err.message } };
}
}

function runErrorMode() {
console.log('Error mode selected — about to throw');
console.warn('This is intentional for testing error display');
throw new Error(
'Intentional error from aerie-action-demo (error mode).\n' +
'This tests the error display in the action run detail view.\n' +
'Stack trace should appear below.'
);
}

async function runSlowMode(verbose) {
const totalMs = 5000;
const steps = 5;
const stepMs = totalMs / steps;

console.log(`Slow mode: running ${steps} steps over ${totalMs}ms`);

for (let i = 1; i <= steps; i++) {
await new Promise(resolve => setTimeout(resolve, stepMs));
const pct = Math.round((i / steps) * 100);
console.log(`Step ${i}/${steps} complete (${pct}%)`);
if (verbose) {
console.log(` Elapsed: ${i * stepMs}ms`);
}
}

return {
status: 'SUCCESS',
data: { message: 'Slow operation completed', durationMs: totalMs, steps },
};
}

exports.main = main;
exports.parameterDefinitions = parameterDefinitions;
exports.settingDefinitions = settingDefinitions;


Loading
Loading