Skip to content

[Manual Test] Create_new_instance_of_-Actor_Framework-_project #5

[Manual Test] Create_new_instance_of_-Actor_Framework-_project

[Manual Test] Create_new_instance_of_-Actor_Framework-_project #5

Workflow file for this run

name: "Process Manual Test Issues"
on:
issues:
# We do NOT trigger on 'unlabeled' to avoid potential errors if label is removed
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:
################################################################
# STEP 1: Capture the event payload (so we can do custom logic)
################################################################
- name: Capture GitHub event payload
run: |
echo "GITHUB_EVENT_PAYLOAD=$(echo '${{ toJson(github.event) }}' | base64 -w0)" >> $GITHUB_ENV
################################################################
# STEP 2: Quick label/title checks => skip if:
# - 'manual-test' label missing
# - Issue title does not start with "[Manual Test]"
################################################################
- name: Preliminary label/title check
id: label_and_title
run: |
EVENT_JSON="$(echo "$GITHUB_EVENT_PAYLOAD" | base64 -d)"
LABELS="$(echo "$EVENT_JSON" | jq -r '.issue.labels[].name')"
ISSUE_TITLE="$(echo "$EVENT_JSON" | jq -r '.issue.title')"
echo "Labels found:"
echo "$LABELS"
echo "Issue title: $ISSUE_TITLE"
# Check exact label
echo "$LABELS" | grep -xq 'manual-test' || {
echo "No 'manual-test' label. Skipping workflow."
echo "::set-output name=skip::true"
exit 0
}
# Check title prefix
if [[ "$ISSUE_TITLE" != "[Manual Test]"* ]]; then
echo "Issue title does not start with '[Manual Test]'. Skipping workflow."
echo "::set-output name=skip::true"
exit 0
fi
echo "::set-output name=skip::false"
- name: Stop if skipping
if: steps.label_and_title.outputs.skip == 'true'
run: |
echo "Skipping further steps..."
exit 0
################################################################
# STEP 3: Parse required form fields (blank-line safe)
################################################################
- name: Parse form fields
id: parse_body
uses: actions/github-script@v6
with:
script: |
const issueBody = `${process.env.ISSUE_BODY || ''}`.trim()
|| `${github.event.issue.body || ''}`.trim();
// Retrieve the next non-blank line after a label
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)) {
let j = i + 1;
while (j < lines.length) {
const nextLine = lines[j].trim();
if (nextLine !== '') {
return nextLine;
}
j++;
}
return null;
}
}
return null;
}
// The fields we consider "required"
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 them
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;
}
// Optional "notes"
const notesLabel = '📝 Notes or Screenshots (optional)';
parsed.notes = getField(notesLabel) || '';
// Output
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);
################################################################
# STEP 4: Extract numeric test ID + Estimate from a table row
################################################################
- name: Extract numeric TestID & Estimate
id: parse_table
uses: actions/github-script@v6
with:
script: |
const aliasRaw = core.getInput('test_id', { required: true });
const body = github.event.issue.body || '';
// Convert underscores => spaces, for case-insensitive compare
const aliasCompare = aliasRaw.replace(/_/g, ' ').toLowerCase();
let numericID = null;
let estimateVal = null;
const lines = body.split('\n');
for (const row of lines) {
const line = row.trim();
// Suppose each relevant row in the table starts with "|"
if (!line.startsWith('|')) continue;
const cells = line.split('|').map(x => x.trim()).filter(Boolean);
// For example: | Actor Creation | *20 Min* | [Some Link](...) |
// Might see cells like: [0]=, [1]=Actor Creation, [2]=*20 Min*, [3]=[Link](URL)...
if (cells.length < 4) continue; // skip invalid table lines
// cells[1] => "Actor Creation" (title)
// cells[2] => "*20 Min*" (estimate)
// cells[3] => "[Some link](...)"
const possibleTitle = cells[1].toLowerCase();
if (possibleTitle === aliasCompare) {
// parse estimate
const estRaw = cells[2] || '';
const estNoAsterisk = estRaw.replace(/\*/g, '').trim();
if (!estNoAsterisk) {
core.setFailed(`No 'Estimate' found for test alias '${aliasRaw}'.`);
return;
}
// We'll attempt to parse it as a number if it looks numeric
// If it's something like "20 Min", we just strip the alpha.
// For a pure numeric field, let's parse out digits:
const numMatch = estNoAsterisk.match(/\d+/);
if (!numMatch) {
core.setFailed(`Estimate does not contain a numeric portion: '${estNoAsterisk}'`);
return;
}
estimateVal = parseInt(numMatch[0], 10);
if (isNaN(estimateVal)) {
core.setFailed(`Cannot parse estimate as number: '${estNoAsterisk}'`);
return;
}
// parse link => 7-digit .md
const linkCell = cells[3];
const linkMatch = linkCell.match(/\(([^)]+)\)/);
if (!linkMatch) {
core.setFailed(`No link found in table for alias='${aliasRaw}'.`);
return;
}
const linkURL = linkMatch[1];
const segs = linkURL.split('/');
const lastSeg = segs[segs.length - 1];
if (!lastSeg.endsWith('.md')) {
core.setFailed(`Link not referencing a .md file => '${linkURL}'`);
return;
}
const baseName = lastSeg.replace('.md', '');
if (!/^\d{7}$/.test(baseName)) {
core.setFailed(`Not a 7-digit .md => '${lastSeg}'`);
return;
}
numericID = baseName;
break;
}
}
if (!numericID) {
core.setFailed(`No matching row found for test alias '${aliasRaw}'.`);
return;
}
// Expose as outputs
core.setOutput('numeric_id', numericID);
core.setOutput('estimate_num', estimateVal.toString());
################################################################
# STEP 5a: Find the previous [Manual Test] issue for same author
################################################################
- name: Find previous [Manual Test] issue
id: find_previous
uses: actions/github-script@v6
with:
script: |
const currentNum = github.event.issue.number;
const authorLogin = github.event.issue.user.login;
const { owner, repo } = context.repo;
// search for issues in this repo
// label=manual-test, author=..., in:title "[Manual Test]"
// sorted desc by created => exclude current
const q = `repo:${owner}/${repo} is:issue label:manual-test author:${authorLogin} in:title "[Manual Test]"`;
const searchResp = await github.rest.search.issuesAndPullRequests({
q,
sort: 'created',
order: 'desc',
per_page: 10
});
const results = searchResp.data.items.filter(i => i.number !== currentNum);
if (!results.length) {
core.info("No previous [Manual Test] issue found for same author => no Start Date to set.");
core.setOutput('previous_issue_number', '');
return;
}
const prevIssue = results[0];
core.info(`Found previous issue #${prevIssue.number}, title="${prevIssue.title}"`);
core.setOutput('previous_issue_number', prevIssue.number.toString());
################################################################
# STEP 5b: Retrieve the previous issue's "End Date"
################################################################
- name: Read previous End Date
id: read_end_date
if: steps.find_previous.outputs.previous_issue_number != ''
uses: actions/github-script@v6
with:
script: |
const prevNum = parseInt(core.getInput('previous_issue_number'), 10);
const { owner, repo } = context.repo;
// fetch the node_id
const issueResp = await github.rest.issues.get({
owner,
repo,
issue_number: prevNum
});
const prevNodeId = issueResp.data.node_id;
if (!prevNodeId) {
core.notice(`No node_id for #${prevNum}, skipping 'End Date' retrieval.`);
return;
}
// find the project item(s)
const itemsQuery = `
query($issueId: ID!) {
node(id: $issueId) {
... on Issue {
projectItems(first: 50) {
nodes {
id
project {
title
}
}
}
}
}
}
`;
const itemData = await github.graphql(itemsQuery, { issueId: prevNodeId });
const items = itemData.node?.projectItems?.nodes || [];
let foundEnd = '';
for (const it of items) {
const fQuery = `
query($itemId: ID!) {
node(id: $itemId) {
... on ProjectV2Item {
fieldValues(first: 50) {
nodes {
__typename
... on ProjectV2ItemFieldDateValue {
field {
... on ProjectV2FieldCommon {
id
name
}
}
date
}
}
}
}
}
}
`;
const fResp = await github.graphql(fQuery, { itemId: it.id });
const fieldNodes = fResp.node?.fieldValues?.nodes || [];
for (const fn of fieldNodes) {
if (fn.field?.name === "End Date" && fn.__typename === "ProjectV2ItemFieldDateValue") {
if (fn.date) {
foundEnd = fn.date;
break;
}
}
}
if (foundEnd) break;
}
core.info(`Previous #${prevNum} => End Date='${foundEnd}'`);
core.setOutput('prev_end_date', foundEnd);
################################################################
# STEP 5c: Update current issue's fields
# (set Start Date, End Date if needed, plus TestID, etc.)
################################################################
- name: Update project fields for current issue
id: update_fields
uses: actions/github-script@v6
with:
script: |
// Gather inputs
const testIdNum = core.getInput('numeric_id'); // from parse_table
const estimateRaw = core.getInput('estimate_num');
const testResult = core.getInput('test_result');
const osUsed = core.getInput('os_used');
const lvVers = core.getInput('labview_version');
const lvBit = core.getInput('labview_bitness');
const notes = core.getInput('notes');
const prevEnd = core.getInput('prev_end_date'); // might be blank
const createdAt = github.event.issue.created_at;
const issueNodeId = github.event.issue.node_id;
if (!issueNodeId) {
core.setFailed("No node_id on current issue. Cannot update project items.");
return;
}
let estimateNum = null;
try {
estimateNum = parseFloat(estimateRaw);
} catch(e) {
// fallback
estimateNum = 0;
}
// 1) Query project items for this issue
const itemsQ = `
query($id: ID!) {
node(id: $id) {
... on Issue {
projectItems(first: 50) {
nodes {
id
project {
id
title
}
}
}
}
}
}
`;
const resp = await github.graphql(itemsQ, { id: issueNodeId });
const items = resp.node?.projectItems?.nodes || [];
if (!items.length) {
core.info("Issue is not assigned to any project => skipping field updates.");
return;
}
// 2) For each item, read current field values => update them
async function readFields(itemId) {
const qry = `
query($itemId: ID!) {
node(id: $itemId) {
... on ProjectV2Item {
fieldValues(first: 50) {
nodes {
__typename
... on ProjectV2ItemFieldTextValue {
field { ... on ProjectV2FieldCommon { id name } }
text
}
... on ProjectV2ItemFieldSingleSelectValue {
field { ... on ProjectV2FieldCommon { id name } }
name
}
... on ProjectV2ItemFieldDateValue {
field { ... on ProjectV2FieldCommon { id name } }
date
}
... on ProjectV2ItemFieldNumberValue {
field { ... on ProjectV2FieldCommon { id name } }
number
}
}
}
}
}
}
`;
const r = await github.graphql(qry, { itemId });
return r.node?.fieldValues?.nodes || [];
}
async function updateFieldValue(projectId, itemId, fieldId, valObj) {
const mut = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $val: ProjectV2FieldValue!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: $val
}) {
projectV2Item { id }
}
}
`;
await github.graphql(mut, {
projectId,
itemId,
fieldId,
val: valObj
});
}
for (const it of items) {
core.info(`Updating itemId='${it.id}' in project='${it.project.title}'`);
const fieldNodes = await readFields(it.id);
// Build map => name -> {id, typeName, currentValue}
const map = {};
for (const fn of fieldNodes) {
const fName = fn.field?.name;
if (!fName) continue;
let value;
if (fn.__typename === 'ProjectV2ItemFieldTextValue') {
value = fn.text;
} else if (fn.__typename === 'ProjectV2ItemFieldSingleSelectValue') {
value = fn.name;
} else if (fn.__typename === 'ProjectV2ItemFieldDateValue') {
value = fn.date;
} else if (fn.__typename === 'ProjectV2ItemFieldNumberValue') {
value = fn.number;
}
map[fName] = {
fieldId: fn.field.id,
typeName: fn.__typename,
current: value
};
}
// Update "TestID" (Text)
if (map["TestID"]) {
await updateFieldValue(it.project.id, it.id, map["TestID"].fieldId, {
text: testIdNum
});
}
// Update "LabVIEW Version" (Text)
if (map["LabVIEW Version"]) {
await updateFieldValue(it.project.id, it.id, map["LabVIEW Version"].fieldId, {
text: lvVers
});
}
// "LabVIEW Bitness" (Text)
if (map["LabVIEW Bitness"]) {
await updateFieldValue(it.project.id, it.id, map["LabVIEW Bitness"].fieldId, {
text: lvBit
});
}
// "Operating System" (Text)
if (map["Operating System"]) {
await updateFieldValue(it.project.id, it.id, map["Operating System"].fieldId, {
text: osUsed
});
}
// "Test Result" (Single Select)
if (map["Test Result"]) {
// If it's singleSelectValue => { singleSelectValue: testResult }
// If it was text => { text: testResult }
if (map["Test Result"].typeName === 'ProjectV2ItemFieldSingleSelectValue') {
await updateFieldValue(it.project.id, it.id, map["Test Result"].fieldId, {
singleSelectValue: testResult
});
} else {
// fallback if it was text
await updateFieldValue(it.project.id, it.id, map["Test Result"].fieldId, {
text: testResult
});
}
}
// "Notes" (Text)
if (map["Notes"]) {
await updateFieldValue(it.project.id, it.id, map["Notes"].fieldId, {
text: notes
});
}
// "Estimate" (Number)
// parse estimateVal from estimateNum
if (map["Estimate"]) {
await updateFieldValue(it.project.id, it.id, map["Estimate"].fieldId, {
number: estimateNum
});
}
// "End Date" => set once only if blank
if (map["End Date"]) {
if (!map["End Date"].current) {
await updateFieldValue(it.project.id, it.id, map["End Date"].fieldId, {
date: createdAt // use issue creation timestamp
});
}
}
// "Start Date" => from prev_end_date if any, only if blank
if (map["Start Date"] && prevEnd) {
if (!map["Start Date"].current) {
await updateFieldValue(it.project.id, it.id, map["Start Date"].fieldId, {
date: prevEnd
});
}
}
core.info("Item updated successfully.");
}
core.info("Finished updating fields for all assigned project items.");
- name: Done
run: echo "Workflow completed successfully."