Skip to content

[Manual Test] Create_new_instance_of_-Actor_Framework-_project #4

[Manual Test] Create_new_instance_of_-Actor_Framework-_project

[Manual Test] Create_new_instance_of_-Actor_Framework-_project #4

Workflow file for this run

name: "Manual Test Workflow"
on:
issues:
types: [opened, edited, labeled]
concurrency:
group: manual-test-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
process-manual-test:
runs-on: ubuntu-latest
steps:
################################################################
# 1. Base64-encode the entire event JSON to avoid shell parsing
################################################################
- name: Prepare event JSON
run: |
echo "GITHUB_EVENT_PAYLOAD=$(echo '${{ toJson(github.event) }}' | base64 -w0)" >> $GITHUB_ENV
################################################################
# 2. Check label "manual-test" and title prefix "[Manual Test]"
# - If missing, skip workflow (do not fail)
################################################################
- name: Check if 'manual-test' label exists & title starts with '[Manual Test]'
id: label_and_title_check
run: |
# Decode from base64
EVENT_JSON="$(echo "$GITHUB_EVENT_PAYLOAD" | base64 -d)"
# Extract labels and title from the decoded JSON
LABELS="$(echo "$EVENT_JSON" | jq -r '.issue.labels[].name')"
ISSUE_TITLE="$(echo "$EVENT_JSON" | jq -r '.issue.title')"
echo "Labels found: $LABELS"
echo "Issue title: $ISSUE_TITLE"
# Check for exact 'manual-test' label
echo "$LABELS" | grep -xq 'manual-test' || {
echo "Label 'manual-test' not present. Skipping."
echo "::set-output name=skip::true"
exit 0
}
# Check if issue title starts with '[Manual Test]'
if [[ "$ISSUE_TITLE" != "[Manual Test]"* ]]; then
echo "Issue title does not start with '[Manual Test]'. Skipping."
echo "::set-output name=skip::true"
exit 0
fi
echo "::set-output name=skip::false"
- name: Stop if skip was requested
if: steps.label_and_title_check.outputs.skip == 'true'
run: |
echo "Skipping workflow."
exit 0
################################################################
# 3. Parse issue body with ENHANCED parser to skip blank lines
################################################################
- name: Parse issue body (enhanced parser)
id: parse_body
uses: actions/github-script@v6
with:
script: |
const issueBody = `${process.env.ISSUE_BODY || ''}`.trim()
|| `${github.event.issue.body || ''}`.trim();
// Updated function to skip blank lines
function getField(fieldLabel) {
const lines = issueBody.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.includes(fieldLabel)) {
// Move to subsequent lines until a non-blank line is found
let j = i + 1;
while (j < lines.length) {
const nextLine = lines[j].trim();
if (nextLine !== '') {
return nextLine;
}
j++;
}
// If we exhaust lines, treat it as missing
return null;
}
}
return null; // Label not found
}
const requiredFields = {
test_id: '🧪 Select a Test',
test_result: '✅ Test Result',
os_used: '🖥️ Operating System',
labview_version: '🧰 LabVIEW Version Used',
labview_bitness: '💻 LabVIEW Bitness'
};
// Parse required fields
let parsed = {};
for (const [key, label] of Object.entries(requiredFields)) {
const val = getField(label);
if (!val) {
core.setFailed(`Missing or invalid field: '${key}' (label: '${label}')`);
return;
}
parsed[key] = val;
}
// Validate test_result
const validResults = ['Passed', 'Failed', 'Needs Review'];
if (!validResults.includes(parsed.test_result)) {
core.setFailed(`Invalid test_result: '${parsed.test_result}'. Must be 'Passed', 'Failed', or 'Needs Review'.`);
return;
}
// Parse optional notes
const notesLabel = '📝 Notes or Screenshots (optional)';
const optionalNotes = getField(notesLabel) || '';
parsed.notes = optionalNotes;
// Output each field so subsequent steps can read them
core.setOutput('test_id', parsed.test_id);
core.setOutput('test_result', parsed.test_result);
core.setOutput('os_used', parsed.os_used);
core.setOutput('labview_version', parsed.labview_version);
core.setOutput('labview_bitness', parsed.labview_bitness);
core.setOutput('notes', parsed.notes);
################################################################
# 4. Example step: Parse table row for numeric TestID + Estimate
################################################################
- name: Find matching table row and parse numeric TestID + Estimate
id: parse_table
uses: actions/github-script@v6
with:
script: |
const issueBody = `${github.event.issue.body || ''}`;
const testAlias = core.getInput('test_id', { stepId: 'parse_body' });
// Convert underscores to spaces, for table matching
const testTitle = testAlias.replace(/_/g, ' ');
// Example naive table parse
const lines = issueBody.split('\n');
let matchedRow = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('|') && line.endsWith('|')) {
if (line.toLowerCase().includes(testTitle.toLowerCase())) {
matchedRow = line;
break;
}
}
}
if (!matchedRow) {
core.setFailed(`No table row found for title '${testTitle}' in the issue body.`);
return;
}
const columns = matchedRow.split('|').map(c => c.trim()).filter(Boolean);
if (columns.length < 3) {
core.setFailed(`Matched row does not have at least 3 columns: '${matchedRow}'`);
return;
}
const rowTitle = columns[0];
const rowEstTime = columns[1];
const rowLink = columns[2];
// Extract numeric .md
const mdRegex = /\/(\\d{7})\\.md/;
const match = rowLink.match(mdRegex);
if (!match) {
core.setFailed(`No 7-digit .md file found in link column: '${rowLink}'`);
return;
}
const numericTestID = match[1];
// Clean up the Est. Time
const cleanedEstTime = rowEstTime.replace(/\*/g, '').trim();
if (!cleanedEstTime) {
core.setFailed(`Est. Time is missing or empty for row: '${matchedRow}'`);
return;
}
core.setOutput('numeric_test_id', numericTestID);
core.setOutput('estimate', cleanedEstTime);
################################################################
# 5. Retrieve or create the project item (and set End date once)
################################################################
- name: Retrieve or create Project Item & set End date (if not set)
id: project_item
uses: actions/github-script@v6
with:
script: |
const issueNodeId = github.event.issue.node_id;
const createdAt = github.event.issue.created_at; // e.g. "2025-04-19T12:34:56Z"
// Node ID of your Project (Beta)
const projectNodeId = process.env.PROJECT_NODE_ID || 'YOUR_PROJECT_NODE_ID';
// Field IDs (replace with your actual IDs)
const fieldId_endDate = process.env.FIELD_ID_ENDDATE || 'ENDDATE_FIELD_ID';
const fieldId_startDate = process.env.FIELD_ID_STARTDATE || 'STARTDATE_FIELD_ID';
core.setOutput('FIELD_ID_ENDDATE', fieldId_endDate);
core.setOutput('FIELD_ID_STARTDATE', fieldId_startDate);
// 1) Query for existing item
const existingItemQuery = `
query($projectId: ID!, $contentId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100, filterBy: {contentId: $contentId}) {
nodes {
id
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldDateValue {
field { id }
date
}
}
}
}
}
}
}
}
`;
let existingResp;
try {
existingResp = await github.graphql(existingItemQuery, {
projectId: projectNodeId,
contentId: issueNodeId
});
} catch (err) {
core.setFailed(`Failed to query project item: ${err.message}`);
return;
}
let projectItemId = null;
let endDateAlreadySet = null;
const items = existingResp.node.items.nodes;
if (items.length > 0) {
projectItemId = items[0].id;
// check if there's an End date
const fieldVals = items[0].fieldValues.nodes;
const endDateField = fieldVals.find(f => f.field.id === fieldId_endDate);
if (endDateField?.date) {
endDateAlreadySet = endDateField.date;
}
} else {
// create item
const createItemMutation = `
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item { id }
}
}
`;
try {
const createResp = await github.graphql(createItemMutation, {
projectId: projectNodeId,
contentId: issueNodeId
});
projectItemId = createResp.addProjectV2ItemById.item.id;
} catch (err) {
core.setFailed(`Failed to create project item: ${err.message}`);
return;
}
}
if (!projectItemId) {
core.setFailed('No project item ID found or created.');
return;
}
// 2) If End date not set, set it now
if (!endDateAlreadySet) {
const updateEndDate = `
mutation($itemId: ID!, $fieldId: ID!, $value: String) {
updateProjectV2ItemFieldValue(
input: { projectId: "${projectNodeId}", itemId: $itemId, fieldId: $fieldId, value: { date: $value } }
) {
projectV2Item { id }
}
}
`;
try {
await github.graphql(updateEndDate, {
itemId: projectItemId,
fieldId: fieldId_endDate,
value: createdAt
});
} catch (err) {
core.setFailed(`Failed to set End date: ${err.message}`);
return;
}
}
core.setOutput('project_item_id', projectItemId);
################################################################
# 6. Find previous `[Manual Test]` issue and get its End date
################################################################
- name: Find the previous [Manual Test] issue's End date (if any)
id: get_previous_end_date
uses: actions/github-script@v6
with:
script: |
const currentAuthor = github.event.issue.user.login;
const currentIssueNumber = github.event.issue.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// Example search:
// "repo:owner/repo is:issue label:manual-test in:title '[Manual Test]' author:theUser sort:created-desc"
const q = `repo:${owner}/${repo} is:issue label:manual-test author:${currentAuthor} in:title "[Manual Test]" sort:created-desc`;
let searchResp;
try {
searchResp = await github.rest.search.issuesAndPullRequests({
q,
per_page: 5
});
} catch (err) {
core.setFailed(`Search for previous issues failed: ${err.message}`);
return;
}
if (!searchResp) return;
const items = searchResp.data.items || [];
// Exclude the current issue
const filtered = items.filter(i => i.number !== currentIssueNumber);
if (filtered.length === 0) {
core.setOutput('previous_end_date', '');
return;
}
// The first in filtered is the "previous" one
const prevIssue = filtered[0];
const projectNodeId = process.env.PROJECT_NODE_ID || 'YOUR_PROJECT_NODE_ID';
const fieldId_endDate = core.getInput('FIELD_ID_ENDDATE', { stepId: 'project_item' });
const prevItemQuery = `
query($projectId: ID!, $contentId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100, filterBy: {contentId: $contentId}) {
nodes {
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldDateValue {
field { id }
date
}
}
}
}
}
}
}
}
`;
try {
const pqResp = await github.graphql(prevItemQuery, {
projectId: projectNodeId,
contentId: prevIssue.node_id
});
const prevItems = pqResp.node.items.nodes;
if (prevItems.length === 0) {
core.setOutput('previous_end_date', '');
return;
}
const fvals = prevItems[0].fieldValues.nodes;
const eDateVal = fvals.find(f => f.field.id === fieldId_endDate);
if (!eDateVal?.date) {
core.setOutput('previous_end_date', '');
} else {
core.setOutput('previous_end_date', eDateVal.date);
}
} catch (err) {
core.setFailed(`Failed to query previous issue item: ${err.message}`);
}
################################################################
# 7. Update the project fields (including Start date, etc.)
################################################################
- name: Update fields (Start date, TestID, Estimate, etc.)
uses: actions/github-script@v6
with:
script: |
const projectItemId = core.getInput('project_item_id', { stepId: 'project_item' });
if (!projectItemId) {
core.setFailed('No project item ID available.');
return;
}
const projectNodeId = process.env.PROJECT_NODE_ID || 'YOUR_PROJECT_NODE_ID';
// Field IDs from environment or from earlier steps
const fieldId_endDate = core.getInput('FIELD_ID_ENDDATE', { stepId: 'project_item' });
const fieldId_startDate = core.getInput('FIELD_ID_STARTDATE', { stepId: 'project_item' });
const fieldId_testID = process.env.FIELD_ID_TESTID || 'TESTID_FIELD_ID';
const fieldId_estimate = process.env.FIELD_ID_ESTIMATE || 'ESTIMATE_FIELD_ID';
const fieldId_testResult = process.env.FIELD_ID_TESTRESULT || 'TESTRESULT_FIELD_ID';
const fieldId_osUsed = process.env.FIELD_ID_OSUSED || 'OSUSED_FIELD_ID';
const fieldId_lvVersion = process.env.FIELD_ID_LVVERSION || 'LVVERSION_FIELD_ID';
const fieldId_lvBitness = process.env.FIELD_ID_LVBITNESS || 'LVBITNESS_FIELD_ID';
const fieldId_notes = process.env.FIELD_ID_NOTES || 'NOTES_FIELD_ID';
// Inputs from previous steps
const numericTestId = core.getInput('numeric_test_id', { stepId: 'parse_table' });
const estimateValue = core.getInput('estimate', { stepId: 'parse_table' });
const testResult = core.getInput('test_result', { stepId: 'parse_body' });
const osUsed = core.getInput('os_used', { stepId: 'parse_body' });
const lvVersion = core.getInput('labview_version', { stepId: 'parse_body' });
const lvBitness = core.getInput('labview_bitness', { stepId: 'parse_body' });
const notes = core.getInput('notes', { stepId: 'parse_body' });
const previousEndDate = core.getInput('previous_end_date', { stepId: 'get_previous_end_date' });
// Build list of fields to update
const fieldsToUpdate = [];
// Start date = previous issue’s End date (if any)
if (previousEndDate) {
fieldsToUpdate.push({
fieldId: fieldId_startDate,
value: { date: previousEndDate }
});
}
// TestID, Estimate, Test Result, OS, etc.
fieldsToUpdate.push({
fieldId: fieldId_testID,
value: { text: numericTestId }
});
fieldsToUpdate.push({
fieldId: fieldId_estimate,
value: { text: estimateValue }
});
fieldsToUpdate.push({
fieldId: fieldId_testResult,
value: { singleSelectOptionId: null, name: testResult }
});
fieldsToUpdate.push({
fieldId: fieldId_osUsed,
value: { text: osUsed }
});
fieldsToUpdate.push({
fieldId: fieldId_lvVersion,
value: { text: lvVersion }
});
fieldsToUpdate.push({
fieldId: fieldId_lvBitness,
value: { text: lvBitness }
});
fieldsToUpdate.push({
fieldId: fieldId_notes,
value: { text: notes }
});
// Execute a GraphQL mutation for each field
for (const field of fieldsToUpdate) {
const { fieldId, value } = field;
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
updateProjectV2ItemFieldValue(
input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: $value
}
) {
projectV2Item {
id
}
}
}
`;
try {
await github.graphql(mutation, {
projectId: projectNodeId,
itemId: projectItemId,
fieldId: fieldId,
value: value
});
} catch (err) {
core.setFailed(`Failed to update field '${fieldId}': ${err.message}`);
return;
}
}
core.notice('Successfully updated project fields!');