[Manual Test] {{ test_id }} by {{ user_name }} #26
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 Issue Processing" | ||
on: | ||
issues: | ||
types: [opened, edited, labeled] | ||
# (Optionally, a manual trigger could be added here via workflow_dispatch if needed for re-running) | ||
concurrency: | ||
group: manual-test-${{ github.event.issue.number }} | ||
cancel-in-progress: true | ||
jobs: | ||
process-manual-test: | ||
runs-on: ubuntu-latest | ||
# Only run this job if the issue has the manual-test label and proper title prefix | ||
if: contains(toJson(github.event.issue.labels), 'manual-test') && startsWith(github.event.issue.title, '[Manual Test]') | ||
steps: | ||
- name: Checkout repository (for issue form template) | ||
uses: actions/checkout@v3 | ||
- name: Parse Issue Form Data | ||
id: parse | ||
uses: issue-ops/[email protected] | ||
with: | ||
body: ${{ github.event.issue.body }} | ||
issue-form-template: manual-test-report.yml | ||
- name: Display Parsed Data (debug) | ||
run: | | ||
echo "Reporter: ${{ steps.parse.outputs.json.user_name }}" | ||
echo "Test ID: ${{ steps.parse.outputs.json.test_id }}" | ||
echo "LabVIEW Version: ${{ steps.parse.outputs.json.labview_version }}" | ||
echo "LabVIEW Bitness: ${{ steps.parse.outputs.json.labview_bitness }}" | ||
echo "Operating System: ${{ steps.parse.outputs.json.os_used }}" | ||
echo "Test Result: ${{ steps.parse.outputs.json.test_result }}" | ||
echo "Notes: ${{ steps.parse.outputs.json.notes }}" | ||
- name: Validate and Prepare Data | ||
id: process | ||
uses: actions/github-script@v6 | ||
env: | ||
FORM_JSON: ${{ steps.parse.outputs.json }} | ||
script: | | ||
Check failure on line 42 in .github/workflows/manual-test.yml
|
||
const form = JSON.parse(process.env.FORM_JSON); | ||
// 1. Validate required fields from the form | ||
const requiredFields = ['test_id', 'labview_version', 'labview_bitness', 'os_used', 'test_result']; | ||
for (const field of requiredFields) { | ||
if (!form[field] || (typeof form[field] === 'string' && form[field].trim() === '')) { | ||
core.setFailed(`❌ Missing or empty field: ${field}`); | ||
return; | ||
} | ||
} | ||
const validResults = ['Passed', 'Failed', 'Needs Review']; | ||
if (!validResults.includes(form.test_result)) { | ||
core.setFailed(`❌ Invalid Test Result: ${form.test_result}`); | ||
return; | ||
} | ||
// 2. Extract numeric part of Test ID (if any) | ||
const match = form.test_id.match(/(\d+)/); | ||
const numeric_test_id = match ? match[1] : ''; | ||
// 3. Scrape reference table in the issue body to get estimated minutes | ||
const body = context.payload.issue.body || ''; | ||
let estimate_min = 0; | ||
const lines = body.split('\n').filter(line => line.startsWith('|')); | ||
const estMap = {}; | ||
for (const line of lines) { | ||
if (line.includes('---')) continue; // skip table header separator | ||
const cols = line.split('|').map(col => col.trim()); | ||
if (cols.length < 3) continue; | ||
const title = cols[1]; | ||
const m = cols[2].match(/(\d+)\s*Min/i); | ||
if (m) { | ||
const key = title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, ''); | ||
estMap[key] = parseInt(m[1], 10); | ||
} | ||
} | ||
const testKey = form.test_id.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, ''); | ||
if (estMap[testKey] !== undefined) { | ||
estimate_min = estMap[testKey]; | ||
} else { | ||
// try partial matching of keys if no exact slug match | ||
for (const [k, v] of Object.entries(estMap)) { | ||
if (testKey.includes(k) || k.includes(testKey)) { | ||
estimate_min = v; | ||
break; | ||
} | ||
} | ||
} | ||
if (estimate_min === 0) { | ||
core.notice(`⚠️ No time estimate found for test "${form.test_id}" (defaulting to 0 minutes).`); | ||
} | ||
// 4. Set outputs for downstream steps | ||
core.setOutput('user_name', form.user_name || ''); | ||
core.setOutput('test_id', form.test_id); | ||
core.setOutput('labview_version', form.labview_version); | ||
core.setOutput('labview_bitness', form.labview_bitness); | ||
core.setOutput('os_used', form.os_used); | ||
core.setOutput('test_result', form.test_result); | ||
core.setOutput('notes', form.notes || ''); | ||
core.setOutput('numeric_test_id', numeric_test_id); | ||
core.setOutput('estimate_min', String(estimate_min)); | ||
- name: Find previous Manual Test issue by author | ||
id: find_previous | ||
uses: actions/github-script@v6 | ||
env: | ||
CUR_ISSUE: ${{ github.event.issue.number }} | ||
script: | | ||
const curIssue = parseInt(process.env.CUR_ISSUE); | ||
const author = context.payload.issue.user.login; | ||
const { owner, repo } = context.repo; | ||
const query = `repo:${owner}/${repo} label:manual-test author:${author} in:title "[Manual Test]" sort:created-desc`; | ||
const result = await github.graphql(`query($q: String!){ | ||
search(query: $q, type: ISSUE, first: 5) { | ||
nodes { ... on Issue { number } } | ||
} | ||
}`, { q: query }); | ||
const issues = result.search.nodes.map(n => n.number); | ||
const prevNum = issues.find(num => num !== curIssue) || null; | ||
core.setOutput('prev_num', prevNum ? String(prevNum) : ''); | ||
if (prevNum) { | ||
core.info(`Found previous manual test issue by ${author}: #${prevNum}`); | ||
} else { | ||
core.info('No previous manual test issue found for this author.'); | ||
} | ||
- name: Read "End Date" from previous issue (if any) | ||
id: read_end | ||
if: steps.find_previous.outputs.prev_num != '' | ||
uses: actions/github-script@v6 | ||
env: | ||
PREV_NUM: ${{ steps.find_previous.outputs.prev_num }} | ||
script: | | ||
const prevNum = parseInt(process.env.PREV_NUM); | ||
const { owner, repo } = context.repo; | ||
// Get the previous issue's node ID via REST | ||
const prevIssue = (await github.rest.issues.get({ owner, repo, issue_number: prevNum })).data; | ||
if (!prevIssue.node_id) { | ||
core.setOutput('prev_end', ''); | ||
return; | ||
} | ||
// Query project items of the previous issue (GraphQL) | ||
const itemsRes = await github.graphql(`query($id: ID!){ | ||
node(id: $id) { ... on Issue { projectItems(first: 50) { nodes { id } } } } | ||
}`, { id: prevIssue.node_id }); | ||
const items = itemsRes.node.projectItems.nodes; | ||
let prevEndDate = ''; | ||
// Loop through project items to find a date in "End Date" field | ||
for (const item of items) { | ||
const fieldsRes = await github.graphql(`query($itemId: ID!){ | ||
node(id: $itemId) { ... on ProjectV2Item { fieldValues(first: 50) { nodes { | ||
... on ProjectV2ItemFieldDateValue { field { ... on ProjectV2FieldCommon { name } } date } | ||
} } } } | ||
}`, { itemId: item.id }); | ||
const dateFields = fieldsRes.node.fieldValues.nodes; | ||
const endField = dateFields.find(f => f.field?.name === 'End Date' && f.date); | ||
if (endField) { | ||
prevEndDate = endField.date; | ||
break; | ||
} | ||
} | ||
core.setOutput('prev_end', prevEndDate); | ||
if (prevEndDate) { | ||
core.info(`Previous End Date found: ${prevEndDate}`); | ||
} | ||
- name: Update project fields for current issue | ||
uses: actions/github-script@v6 | ||
env: | ||
NUM_ID: ${{ steps.process.outputs.numeric_test_id }} | ||
ESTIMATE_MIN: ${{ steps.process.outputs.estimate_min }} | ||
TEST_RESULT: ${{ steps.process.outputs.test_result }} | ||
OS_USED: ${{ steps.process.outputs.os_used }} | ||
LV_VER: ${{ steps.process.outputs.labview_version }} | ||
LV_BIT: ${{ steps.process.outputs.labview_bitness }} | ||
NOTES: ${{ steps.process.outputs.notes }} | ||
PREV_END: ${{ steps.read_end.outputs.prev_end }} | ||
script: | | ||
const { | ||
NUM_ID, ESTIMATE_MIN, TEST_RESULT, | ||
OS_USED, LV_VER, LV_BIT, NOTES, PREV_END | ||
} = process.env; | ||
const issueNodeId = context.payload.issue.node_id; | ||
if (!issueNodeId) { | ||
core.notice('⚠️ Cannot update project fields: issue node_id not found.'); | ||
return; | ||
} | ||
// Format the current issue creation date (YYYY-MM-DD) for date fields | ||
const createdDate = context.payload.issue.created_at.substring(0, 10); | ||
// Fetch all project items (if the issue is in multiple projects) | ||
const itemsRes = await github.graphql(`query($id: ID!){ | ||
node(id: $id) { ... on Issue { projectItems(first: 50) { nodes { id project { id title } } } } } | ||
}`, { id: issueNodeId }); | ||
const projectItems = itemsRes.node.projectItems.nodes; | ||
if (projectItems.length === 0) { | ||
core.notice('ℹ️ Issue is not part of any project – no project fields to update.'); | ||
return; | ||
} | ||
for (const item of projectItems) { | ||
core.startGroup(`Updating fields in project: ${item.project.title}`); | ||
// Retrieve all field values for this project item | ||
const fieldsRes = await github.graphql(`query($itemId: ID!){ | ||
node(id: $itemId) { ... on ProjectV2Item { fieldValues(first: 50) { nodes { | ||
__typename | ||
... on ProjectV2ItemFieldTextValue { field { ... on ProjectV2FieldCommon { id name } } text } | ||
... on ProjectV2ItemFieldNumberValue { field { ... on ProjectV2FieldCommon { id name } } number } | ||
... on ProjectV2ItemFieldDateValue { field { ... on ProjectV2FieldCommon { id name } } date } | ||
... on ProjectV2ItemFieldSingleSelectValue { field { ... on ProjectV2FieldCommon { id name } } name } | ||
} } } } | ||
}`, { itemId: item.id }); | ||
const fieldNodes = fieldsRes.node.fieldValues.nodes; | ||
// Map field name to its ID and current type | ||
const fieldMap = {}; | ||
for (const f of fieldNodes) { | ||
if (f.field?.name) { | ||
fieldMap[f.field.name] = { id: f.field.id, type: f.__typename }; | ||
} | ||
} | ||
// Define desired updates for simple fields (text or number fields) | ||
const updates = [ | ||
['TestID', { text: NUM_ID }], | ||
['LabVIEW Version', { text: LV_VER }], | ||
['LabVIEW Bitness', { text: LV_BIT }], | ||
['Operating System', { text: OS_USED }], | ||
['Notes', { text: NOTES }], | ||
['Estimate', { number: parseFloat(ESTIMATE_MIN || '0') }] | ||
]; | ||
for (const [fieldName, value] of updates) { | ||
if (fieldMap[fieldName]) { | ||
await github.graphql(`mutation($proj: ID!, $item: ID!, $field: ID!, $val: ProjectV2FieldValue!) { | ||
updateProjectV2ItemFieldValue(input: { projectId: $proj, itemId: $item, fieldId: $field, value: $val }) { projectV2Item { id } } | ||
}`, { proj: item.project.id, item: item.id, field: fieldMap[fieldName].id, val: value }); | ||
} | ||
} | ||
// Update Test Result field (single-select or text field) | ||
if (fieldMap['Test Result']) { | ||
const isSingleSelect = fieldMap['Test Result'].type === 'ProjectV2ItemFieldSingleSelectValue'; | ||
const resultValue = isSingleSelect | ||
? { singleSelectValue: TEST_RESULT } | ||
: { text: TEST_RESULT }; | ||
await github.graphql(`mutation($proj: ID!, $item: ID!, $field: ID!, $val: ProjectV2FieldValue!) { | ||
updateProjectV2ItemFieldValue(input: { projectId: $proj, itemId: $item, fieldId: $field, value: $val }) { projectV2Item { id } } | ||
}`, { proj: item.project.id, item: item.id, field: fieldMap['Test Result'].id, val: resultValue }); | ||
} | ||
// Update date fields: End Date = current issue date, Start Date = prev issue's End Date (if available) | ||
if (fieldMap['End Date']) { | ||
await github.graphql(`mutation($proj: ID!, $item: ID!, $field: ID!, $val: ProjectV2FieldValue!) { | ||
updateProjectV2ItemFieldValue(input: { projectId: $proj, itemId: $item, fieldId: $field, value: $val }) { projectV2Item { id } } | ||
}`, { proj: item.project.id, item: item.id, field: fieldMap['End Date'].id, val: { date: createdDate } }); | ||
} | ||
if (PREV_END && fieldMap['Start Date']) { | ||
await github.graphql(`mutation($proj: ID!, $item: ID!, $field: ID!, $val: ProjectV2FieldValue!) { | ||
updateProjectV2ItemFieldValue(input: { projectId: $proj, itemId: $item, fieldId: $field, value: $val }) { projectV2Item { id } } | ||
}`, { proj: item.project.id, item: item.id, field: fieldMap['Start Date'].id, val: { date: PREV_END } }); | ||
} | ||
core.endGroup(); | ||
} | ||
- name: Archive test report to JSON log | ||
uses: actions/github-script@v6 | ||
env: | ||
FORM_DATA: ${{ steps.parse.outputs.json }} | ||
script: | | ||
const { owner, repo } = context.repo; | ||
const path = 'docs/test_reports.json'; | ||
// Fetch existing JSON log (if it exists in the repo) | ||
let currentData = []; | ||
let fileSha; | ||
try { | ||
const res = await github.rest.repos.getContent({ owner, repo, path }); | ||
fileSha = res.data.sha; | ||
currentData = JSON.parse(Buffer.from(res.data.content, 'base64').toString()); | ||
} catch (error) { | ||
if (error.status !== 404) throw error; | ||
// if 404, file doesn't exist yet, we'll create it | ||
} | ||
const form = JSON.parse(process.env.FORM_DATA); | ||
const issueNum = context.payload.issue.number; | ||
// Remove any existing entry for this issue | ||
currentData = currentData.filter(record => record.issue_number !== issueNum); | ||
// Append new record | ||
currentData.push({ | ||
issue_number: issueNum, | ||
user_name: form.user_name, | ||
test_id: form.test_id, | ||
labview_version: form.labview_version, | ||
labview_bitness: form.labview_bitness, | ||
os_used: form.os_used, | ||
test_result: form.test_result, | ||
notes: form.notes || '', | ||
created_at: context.payload.issue.created_at | ||
}); | ||
// Sort records by issue_number for consistency | ||
currentData.sort((a, b) => a.issue_number - b.issue_number); | ||
// Update or create the JSON file in the repo | ||
const contentEncoded = Buffer.from(JSON.stringify(currentData, null, 2)).toString('base64'); | ||
await github.rest.repos.createOrUpdateFileContents({ | ||
owner, repo, path, | ||
message: `Update test_reports.json for issue #${issueNum}`, | ||
content: contentEncoded, | ||
sha: fileSha | ||
}); | ||
- name: Auto-assign issue to reporter | ||
uses: kentaro-m/[email protected] | ||
with: | ||
repo-token: ${{ secrets.GITHUB_TOKEN }} | ||
assignees: "author" | ||
- name: ✅ Done | ||
run: echo "Manual test processing workflow completed successfully." |