[Manual Test] {{ test_id }} by {{ user_name }} #29
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] | |
concurrency: | |
group: manual-test-${{ github.event.issue.number }} | |
cancel-in-progress: true | |
jobs: | |
process-manual-test: | |
if: contains(toJson(github.event.issue.labels), 'manual-test') | |
runs-on: ubuntu-latest | |
steps: | |
- name: Warn if title prefix missing | |
if: ${{ !startsWith(github.event.issue.title, '[Manual Test]') }} | |
run: echo "::warning ::Issue title does not start with '[Manual Test]'" | |
- uses: actions/checkout@v3 | |
# ---------------- Parse Issue‑Form ---------------- | |
- name: Parse form with IssueOps Parser | |
id: parse | |
uses: issue-ops/[email protected] | |
with: | |
body: ${{ github.event.issue.body }} | |
issue-form-template: manual-test-report.yml | |
# -------------- Validate + table extraction -------------- | |
- name: Validate & extract table row | |
id: process | |
uses: actions/github-script@v6 | |
env: | |
FORM_JSON: ${{ steps.parse.outputs.json }} | |
with: | |
script: | | |
const form = JSON.parse(process.env.FORM_JSON); | |
const required = ['test_id','labview_version','labview_bitness','os_used','test_result']; | |
for (const f of required) { | |
if (!form[f] || String(form[f]).trim() === '') { | |
core.setFailed(`Missing required field: ${f}`); return; | |
} | |
} | |
const ok = ['Passed','Failed','Needs Review']; | |
if (!ok.includes(form.test_result)) { | |
core.setFailed(`Invalid Test Result: ${form.test_result}`); return; | |
} | |
// locate markdown table | |
const body = context.payload.issue.body; | |
const rows = body.split('\n').filter(l=>l.startsWith('|')); | |
if (rows.length===0) core.setFailed('No markdown table found.'); | |
const key = form.test_id.replace(/_/g,' ').toLowerCase(); | |
let num='', est=''; | |
for (const r of rows){ | |
const cols=r.split('|').map(c=>c.trim()); | |
if (cols.length<4) continue; | |
if (cols[1].toLowerCase()!==key) continue; | |
est = cols[2].replace(/\*/g,'').trim(); | |
if (!est) core.setFailed(`No Est. Time for "${cols[1]}"`); | |
const m = cols[3].match(/\/([0-9]{7})\.md\)?$/); | |
if (!m) core.setFailed(`Link does not contain 7‑digit .md`); | |
num = m[1]; | |
break; | |
} | |
if(!num) core.setFailed(`No matching row for alias ${form.test_id}`); | |
core.setOutput('numeric_test_id', num); | |
core.setOutput('estimate_text', est); | |
core.setOutput('test_result', form.test_result); | |
core.setOutput('os_used', form.os_used); | |
core.setOutput('labview_version', form.labview_version); | |
core.setOutput('labview_bitness', form.labview_bitness); | |
core.setOutput('notes', form.notes || ''); | |
core.info(`Parsed row → ID=${num}, Est=${est}`); | |
# ---------------- Find previous manual‑test issue ---------------- | |
- name: Locate previous issue by same author | |
id: previous | |
uses: actions/github-script@v6 | |
env: | |
CUR_NUMBER: ${{ github.event.issue.number }} | |
with: | |
script: | | |
const {owner,repo} = context.repo; | |
const cur = parseInt(process.env.CUR_NUMBER,10); | |
const author = context.payload.issue.user.login; | |
const query = `repo:${owner}/${repo} label:manual-test author:${author} in:title "[Manual Test]" sort:created-desc`; | |
const res = await github.graphql( | |
`query($q:String!){search(query:$q,type:ISSUE,first:10){ | |
nodes{... on Issue{number projectItems(first:50){ | |
nodes{id fieldValues(first:50){ | |
nodes{... on ProjectV2ItemFieldDateValue{field{name} date}} | |
}}}}}}`, {q:query}); | |
let prevEnd=''; | |
for (const n of res.search.nodes){ | |
if (n.number===cur) continue; | |
for (const it of n.projectItems.nodes){ | |
for (const fv of it.fieldValues.nodes){ | |
if (fv.field?.name==='End Date' && fv.date){ prevEnd=fv.date; break; } | |
} | |
if(prevEnd) break; | |
} | |
if(prevEnd) break; | |
} | |
core.setOutput('prev_end', prevEnd); | |
core.info(prevEnd ? `Previous End Date: ${prevEnd}` : 'No previous End Date'); | |
# ---------------- Ensure project item & update fields ---------------- | |
- name: Create / update project fields | |
uses: actions/github-script@v6 | |
env: | |
NUM_ID: ${{ steps.process.outputs.numeric_test_id }} | |
EST: ${{ steps.process.outputs.estimate_text }} | |
RESULT: ${{ steps.process.outputs.test_result }} | |
OS: ${{ steps.process.outputs.os_used }} | |
LV: ${{ steps.process.outputs.labview_version }} | |
BIT: ${{ steps.process.outputs.labview_bitness }} | |
NOTES: ${{ steps.process.outputs.notes }} | |
PREV: ${{ steps.previous.outputs.prev_end }} | |
with: | |
script: | | |
const {owner,repo} = context.repo; | |
const issueNode = context.payload.issue.node_id; | |
const created = context.payload.issue.created_at.substring(0,19)+'Z'; | |
// existing items | |
const q = await github.graphql(`query($id:ID!){node(id:$id){ | |
... on Issue{projectItems(first:50){nodes{id project{id title}}}}}`,{id:issueNode}); | |
let items=q.node.projectItems.nodes; | |
// if none, add to first repo project | |
if(items.length===0){ | |
const plist = await github.graphql(`query($o:String!,$r:String!){ | |
repository(owner:$o,name:$r){projectsV2(first:1){nodes{id title}}}}`,{o:owner,r:repo}); | |
const proj = plist.repository.projectsV2.nodes[0]; | |
if(!proj) core.setFailed('No repository project found.'); | |
const add = await github.graphql(`mutation($p:ID!,$c:ID!){ | |
addProjectV2ItemById(input:{projectId:$p,contentId:$c}){item{id}}}`,{p:proj.id,c:issueNode}); | |
items=[{id:add.addProjectV2ItemById.item.id,project:proj}]; | |
core.notice(`Created project item in "${proj.title}"`); | |
} | |
// helper | |
async function setField(pid,iid,fid,val){ | |
await github.graphql( | |
`mutation($p:ID!,$i:ID!,$f:ID!,$v:ProjectV2FieldValue!){ | |
updateProjectV2ItemFieldValue(input:{projectId:$p,itemId:$i,fieldId:$f,value:$v}){ | |
projectV2Item{id}}}`,{p:pid,i:iid,f:fid,v:val}); | |
} | |
for (const it of items){ | |
const fv = await github.graphql(`query($id:ID!){node(id:$id){ | |
... on ProjectV2Item{fieldValues(first:50){nodes{ | |
__typename | |
... on ProjectV2ItemFieldTextValue{field{id name} text} | |
... on ProjectV2ItemFieldNumberValue{field{id name} number} | |
... on ProjectV2ItemFieldDateValue{field{id name} date} | |
... on ProjectV2ItemFieldSingleSelectValue{field{id name} name} | |
}}}}`,{id:it.id}); | |
const map={}; | |
for(const n of fv.node.fieldValues.nodes) map[n.field.name]={id:n.field.id,type:n.__typename,val:n}; | |
// End Date (immutable) | |
if(map['End Date'] && !map['End Date'].val.date){ | |
await setField(it.project.id,it.id,map['End Date'].id,{date:created}); | |
} | |
// Start Date | |
if(process.env.PREV && map['Start Date']){ | |
await setField(it.project.id,it.id,map['Start Date'].id,{date:process.env.PREV}); | |
} | |
// simple text | |
const txt=[ | |
['TestID',process.env.NUM_ID], | |
['Estimate',process.env.EST], | |
['Operating System',process.env.OS], | |
['LabVIEW Version',process.env.LV], | |
['LabVIEW Bitness',process.env.BIT], | |
['Notes',process.env.NOTES] | |
]; | |
for(const [n,v] of txt){ | |
if(map[n]) await setField(it.project.id,it.id,map[n].id,{text:v}); | |
} | |
// Test Result | |
if(map['Test Result']){ | |
const isSelect = map['Test Result'].type==='ProjectV2ItemFieldSingleSelectValue'; | |
const value = isSelect ? {singleSelectOptionId: map['Test Result'].val.id} : {text:process.env.RESULT}; | |
await setField(it.project.id,it.id,map['Test Result'].id,value); | |
} | |
} | |
# ---------------- Archive JSON ---------------- | |
- name: Archive test report | |
uses: actions/github-script@v6 | |
env: | |
FORM: ${{ steps.parse.outputs.json }} | |
NUMID: ${{ steps.process.outputs.numeric_test_id }} | |
EST: ${{ steps.process.outputs.estimate_text }} | |
with: | |
script: | | |
const {owner,repo} = context.repo; | |
const path='docs/test_reports.json'; | |
let sha=null, data=[]; | |
try{ | |
const res=await github.rest.repos.getContent({owner,repo,path}); | |
sha=res.data.sha; | |
data=JSON.parse(Buffer.from(res.data.content,'base64').toString()); | |
}catch(e){ if(e.status!==404) throw e; } | |
const issueNum=context.payload.issue.number; | |
data=data.filter(r=>r.issue_number!==issueNum); | |
const f=JSON.parse(process.env.FORM); | |
data.push({ | |
issue_number: issueNum, | |
test_id: process.env.NUMID, | |
estimate: process.env.EST, | |
labview_version: f.labview_version, | |
labview_bitness: f.labview_bitness, | |
os_used: f.os_used, | |
test_result: f.test_result, | |
notes: f.notes || '', | |
created_at: context.payload.issue.created_at | |
}); | |
data.sort((a,b)=>a.issue_number-b.issue_number); | |
await github.rest.repos.createOrUpdateFileContents({ | |
owner,repo,path,sha, | |
message:`Update test_reports.json for #${issueNum}`, | |
content:Buffer.from(JSON.stringify(data,null,2)).toString('base64') | |
}); | |
# -------------- Auto‑assign author -------------- | |
- name: Auto‑assign issue to reporter | |
uses: kentaro-m/[email protected] | |
with: | |
repo-token: ${{ secrets.GITHUB_TOKEN }} | |
assignees: author | |
- run: echo "✅ Done – Manual‑Test workflow completed successfully." |