[Manual Test] {{ test_id }} by {{ user_name }} #24
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: "Process Manual Test Issues" | |
######################################################################## | |
# EVENT | |
######################################################################## | |
on: | |
issues: | |
types: [opened, edited, labeled] # we do not trigger on βunβlabeledβ | |
######################################################################## | |
# SERIALISE BY ISSUE | |
######################################################################## | |
concurrency: | |
group: manual-test-${{ github.event.issue.number }} | |
cancel-in-progress: true | |
######################################################################## | |
# MAIN JOB | |
######################################################################## | |
jobs: | |
process-manual-test: | |
runs-on: ubuntu-latest | |
steps: | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 1) Persist the full event payload (base64) β rockβsolid in later | |
# githubβscript steps, no undefinedβproperty errors ever again. | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Capture GitHub event payload | |
run: | | |
echo "GITHUB_EVENT_PAYLOAD=$(echo '${{ toJson(github.event) }}' | base64 -w0)" >> $GITHUB_ENV | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 2) Fast gate β bail if itβs not aΒ manualβtest issue | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Preliminary label/title gate | |
id: prelim | |
run: | | |
EVENT_JSON="$(echo "$GITHUB_EVENT_PAYLOAD" | base64 -d)" | |
LABELS="$(echo "$EVENT_JSON" | jq -r '.issue.labels[].name')" | |
TITLE="$(echo "$EVENT_JSON" | jq -r '.issue.title')" | |
if ! echo "$LABELS" | grep -xq 'manual-test'; then | |
echo "skip=true" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
if [[ "$TITLE" != "[Manual Test]"* ]]; then | |
echo "skip=true" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
echo "skip=false" >> $GITHUB_OUTPUT | |
- name: Abort early (not a manual test) | |
if: steps.prelim.outputs.skip == 'true' | |
run: echo "βοΈ Not a manualβtest issue β workflow exited." | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 3) Parse form fields + scrape the markdown table for Estimate | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Parse form & gather Estimate | |
id: parse_body | |
uses: actions/github-script@v6 | |
with: | |
script: | | |
const payload = JSON.parse( | |
Buffer.from(process.env.GITHUB_EVENT_PAYLOAD,'base64').toString() | |
); | |
const body = (payload.issue.body || '').trim(); | |
//---------------------------------------------------------------- | |
// helpers | |
//---------------------------------------------------------------- | |
const getField = label => { | |
const lines = body.split('\n'); | |
for (let i=0;i<lines.length;i++){ | |
if (lines[i].includes(label)){ | |
for (let j=i+1;j<lines.length;j++){ | |
const t = lines[j].trim(); | |
if (t) return t; | |
} | |
return null; | |
} | |
} | |
return null; | |
}; | |
const slug = s => s.toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_|_$/g,''); | |
//---------------------------------------------------------------- | |
// required dropdowns / inputs | |
//---------------------------------------------------------------- | |
const mapping = { | |
test_id: 'π§ͺ Select a Test', | |
labview_version: 'π§° LabVIEW Version Used', | |
labview_bitness: 'π» LabVIEW Bitness', | |
os_used: 'π₯οΈ Operating System', | |
test_result: 'β Test Result' | |
}; | |
const out = {}; | |
for (const [k,lbl] of Object.entries(mapping)){ | |
const v = getField(lbl); | |
if (!v){ core.setFailed(`Missing β${lbl}β`); return; } | |
out[k]=v; | |
} | |
if (!['Passed','Failed','Needs Review'].includes(out.test_result)){ | |
core.setFailed(`Invalid Test Result Β«${out.test_result}Β»`); return; | |
} | |
out.notes = getField('π Notes or Screenshots (optional)') || ''; | |
//---------------------------------------------------------------- | |
// Scrape the reference table for Estimate | |
//---------------------------------------------------------------- | |
const rows = body.split('\n').filter(l=>l.startsWith('|')); | |
const estMap={}; | |
for (const r of rows){ | |
if (r.includes('---')) continue; | |
const c = r.split('|').map(x=>x.trim()); | |
if (c.length<3) continue; | |
const title=c[1]; | |
const m = c[2].match(/(\d+)\s*Min/i); | |
if(!m) continue; | |
estMap[slug(title)] = parseInt(m[1],10); | |
} | |
const alias = slug(out.test_id); | |
let est = estMap[alias]; | |
if(est===undefined){ | |
for(const [k,v] of Object.entries(estMap)){ | |
if(alias.includes(k)||k.includes(alias)){ est=v; break; } | |
} | |
} | |
if(est===undefined){ core.notice(`No estimate for Β«${out.test_id}Β»`); est=0; } | |
out.estimate_num = String(est); | |
//---------------------------------------------------------------- | |
// numeric part of TestID (if present) | |
//---------------------------------------------------------------- | |
const mDigits = out.test_id.match(/(\d+)/); | |
out.numeric_test_id = mDigits ? mDigits[1] : ''; | |
//---------------------------------------------------------------- | |
// expose outputs | |
//---------------------------------------------------------------- | |
for (const [k,v] of Object.entries(out)) core.setOutput(k,v); | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 4) Locate authorβs previous β[Manual Test]β issue (GraphQL search) | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Find prior ManualβTest issue by author | |
id: find_previous | |
uses: actions/github-script@v6 | |
env: | |
CUR_NUM: ${{ github.event.issue.number }} | |
with: | |
script: | | |
const cur = parseInt(process.env.CUR_NUM,10); | |
const author = context.payload.issue.user.login; | |
const {owner,repo} = context.repo; | |
const qstr = `repo:${owner}/${repo} label:manual-test author:${author} `+ | |
`in:title "[Manual Test]" sort:created-desc`; | |
const gql = `query($q:String!){ | |
search(query:$q,type:ISSUE,first:20){ | |
nodes{... on Issue{number}} | |
}}`; | |
const res = await github.graphql(gql,{q:qstr}); | |
const prev = res.search.nodes.find(n=>n.number!==cur); | |
core.setOutput('prev_num', prev ? String(prev.number) : ''); | |
if(prev) core.info(`Previous issue β #${prev.number}`); | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 5) Read EndΒ Date from that previous issue (if any) | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Read End Date from previous issue | |
id: read_end | |
if: steps.find_previous.outputs.prev_num != '' | |
uses: actions/github-script@v6 | |
env: | |
PREV_NUM: ${{ steps.find_previous.outputs.prev_num }} | |
with: | |
script: | | |
const prev = parseInt(process.env.PREV_NUM,10); | |
const {owner,repo} = context.repo; | |
const prevIssue = (await github.rest.issues.get({owner,repo,issue_number:prev})).data; | |
const node = prevIssue.node_id; | |
if(!node){ core.setOutput('prev_end',''); return; } | |
const qItems=`query($id:ID!){node(id:$id){... on Issue{projectItems(first:50){nodes{id}}}}}`; | |
const items=(await github.graphql(qItems,{id:node})).node.projectItems.nodes; | |
const qFields=`query($i:ID!){node(id:$i){... on ProjectV2Item{fieldValues(first:50){ | |
nodes{... on ProjectV2ItemFieldDateValue{field{... on ProjectV2FieldCommon{name}} date}} | |
}}}`; | |
let end=''; | |
for(const it of items){ | |
const f=(await github.graphql(qFields,{i:it.id})).node.fieldValues.nodes; | |
const hit=f.find(d=>d.field?.name==='End Date' && d.date); | |
if(hit){ end=hit.date; break; } | |
} | |
core.setOutput('prev_end',end); | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 6) Update project custom fields for *this* issue | |
#βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Update project fields | |
uses: actions/github-script@v6 | |
env: | |
NUM_ID: ${{ steps.parse_body.outputs.numeric_test_id }} | |
ESTIMATE_MIN: ${{ steps.parse_body.outputs.estimate_num }} | |
TEST_RESULT: ${{ steps.parse_body.outputs.test_result }} | |
OS_USED: ${{ steps.parse_body.outputs.os_used }} | |
LV_VER: ${{ steps.parse_body.outputs.labview_version }} | |
LV_BIT: ${{ steps.parse_body.outputs.labview_bitness }} | |
NOTES: ${{ steps.parse_body.outputs.notes }} | |
PREV_END: ${{ steps.read_end.outputs.prev_end }} | |
with: | |
script: | | |
const { | |
NUM_ID,ESTIMATE_MIN,TEST_RESULT,OS_USED,LV_VER, | |
LV_BIT,NOTES,PREV_END | |
} = process.env; | |
const issueNode = context.payload.issue.node_id; | |
if(!issueNode){ core.notice('No node_id β cannot update projects'); return; } | |
// creation date in YYYYβMMβDD (for End Date) | |
const createdDate = context.payload.issue.created_at.slice(0,10); | |
//---------------------------------------------------------------- | |
// helpers | |
//---------------------------------------------------------------- | |
const queryItems=`query($id:ID!){node(id:$id){... on Issue{ | |
projectItems(first:50){nodes{id project{id title}}}}}}`; | |
const items=(await github.graphql(queryItems,{id:issueNode})) | |
.node.projectItems.nodes; | |
if(!items.length){ core.notice('Issue not in any project'); return;} | |
const readFields=async item=>{ | |
const q=`query($it:ID!){node(id:$it){... 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} | |
}}}}`; | |
return (await github.graphql(q,{it:item})).node.fieldValues.nodes; | |
}; | |
const setField = async (proj,item,field,val) =>{ | |
const m=`mutation($p:ID!,$i:ID!,$f:ID!,$v:ProjectV2FieldValue!){ | |
updateProjectV2ItemFieldValue(input:{ | |
projectId:$p,itemId:$i,fieldId:$f,value:$v}){projectV2Item{id}}}`; | |
await github.graphql(m,{p:proj,i:item,f:field,v:val}); | |
}; | |
//---------------------------------------------------------------- | |
// iterate project items | |
//---------------------------------------------------------------- | |
for(const it of items){ | |
core.startGroup(`π ${it.project.title}`); | |
const fNodes=await readFields(it.id); | |
const map={}; | |
for(const fn of fNodes){ | |
const n=fn.field?.name; if(!n)continue; | |
map[n]={id:fn.field.id,type:fn.__typename, | |
cur: fn.__typename==='ProjectV2ItemFieldTextValue' ? fn.text : | |
fn.__typename==='ProjectV2ItemFieldNumberValue' ? fn.number : | |
fn.__typename==='ProjectV2ItemFieldDateValue' ? fn.date : | |
fn.__typename==='ProjectV2ItemFieldSingleSelectValue' ? fn.name : '' | |
}; | |
} | |
//------------------------------------------------------------ | |
// simple text / number updates (overwrite unconditionally) | |
//------------------------------------------------------------ | |
const upd=[ | |
['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 [name,val] of upd){ | |
if(map[name]) await setField(it.project.id,it.id,map[name].id,val); | |
} | |
//------------------------------------------------------------ | |
// Singleβselect TestΒ Result | |
//------------------------------------------------------------ | |
if(map['Test Result']){ | |
const v = map['Test Result'].type==='ProjectV2ItemFieldSingleSelectValue' | |
? {singleSelectValue:TEST_RESULT} | |
: {text:TEST_RESULT}; | |
await setField(it.project.id,it.id,map['Test Result'].id,v); | |
} | |
//------------------------------------------------------------ | |
// Date fieldsΒ β strictly follow new rule | |
//------------------------------------------------------------ | |
if(map['End Date']) | |
await setField(it.project.id,it.id,map['End Date'].id,{date:createdDate}); | |
if(PREV_END && map['Start Date']) | |
await setField(it.project.id,it.id,map['Start Date'].id,{date:PREV_END}); | |
core.endGroup(); | |
} | |
- name: β Done | |
run: echo "Workflow finished successfully." |