[Manual Test] Create_new_instance_of_-Actor_Framework-_project #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!'); |