[Manual Test] {{ test_id }} by {{ user_name }} #14
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 | |
on: | |
issues: | |
types: [opened, edited, labeled] # we still ignore βunlabeledβ | |
concurrency: | |
group: manual-test-${{ github.event.issue.number }} | |
cancel-in-progress: true | |
jobs: | |
process-manual-test: | |
runs-on: ubuntu-latest | |
steps: | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 1Β Save the entire event payload (so every script has it) | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Capture GitHub event payload | |
run: | | |
echo "GITHUB_EVENT_PAYLOAD=$(echo '${{ toJson(github.event) }}' | base64 -w0)" >> "$GITHUB_ENV" | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 2Β Quick gateΒ β title must start β\[ManualΒ Test]β & have label | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Preliminary label/title check | |
id: label_and_title | |
run: | | |
evt=$(echo "$GITHUB_EVENT_PAYLOAD" | base64 -d) | |
labels=$(echo "$evt" | jq -r '.issue.labels[].name') | |
title=$( echo "$evt" | 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: Stop early when skipping | |
if: steps.label_and_title.outputs.skip == 'true' | |
run: echo "βοΈ Not a ManualβTest issue β workflow finished." && exit 0 | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 3Β Parse the issue body and export fields | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Parse form fields | |
id: parse_body | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const {core} = global; | |
const payload = JSON.parse(Buffer.from(process.env.GITHUB_EVENT_PAYLOAD,'base64')); | |
const body = (payload.issue.body || '').trim(); | |
// helper β ignore blank lines | |
const getField = label => { | |
const lines = body.split('\n'); | |
for (let i = 0; i < lines.length; i++) { | |
if (!lines[i].includes(label)) continue; | |
for (let j = i + 1; j < lines.length; j++) { | |
const t = lines[j].trim(); | |
if (t) return t; | |
} | |
return null; | |
} | |
return null; | |
}; | |
// required fields | |
const map = { | |
test_id: 'π§ͺ Select a Test', | |
test_result: 'β Test Result', | |
os_used: 'π₯οΈ Operating System', | |
labview_version: 'π§° LabVIEW Version Used', | |
labview_bitness: 'π» LabVIEW Bitness' | |
}; | |
const out = {}; | |
for (const [k,lbl] of Object.entries(map)) { | |
const v = getField(lbl); | |
if (!v) { core.setFailed(`Missing '${lbl}'`); return; } | |
out[k] = v; | |
} | |
// validate test result | |
if (!['Passed','Failed','Needs Review'].includes(out.test_result)) { | |
core.setFailed(`Invalid TestΒ Result '${out.test_result}'`); return; | |
} | |
// optional notes | |
out.notes = getField('π Notes or Screenshots (optional)') || ''; | |
// surface everything as outputs | |
for (const [k,v] of Object.entries(out)) core.setOutput(k,v); | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 4Β Extract numeric part of TestID (used later) | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Extract numeric TestID | |
id: extract_id | |
run: | | |
raw="${{ steps.parse_body.outputs.test_id }}" | |
num=$(echo "$raw" | grep -oE '[0-9]+' || true) | |
if [ -z "$num" ]; then | |
echo "β Cannot derive numeric TestID from '$raw'" >&2; exit 1; fi | |
echo "numeric_test_id=$num" >> "$GITHUB_OUTPUT" | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 5aΒ Locate previous ManualβTest issue by the same author β FIXED | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Find previous [Manual Test] issue | |
id: find_previous | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const {github,context,core} = global; | |
const currentNum = context.issue.number; // β fixed | |
const authorLogin = context.payload.issue.user.login; // β fixed | |
const {owner,repo} = context.repo; | |
const q = `repo:${owner}/${repo} is:issue label:manual-test ` | |
+ `author:${authorLogin} in:title "[Manual Test]"`; | |
const {data:{items}} = await github.rest.search.issuesAndPullRequests({ | |
q, sort:'created', order:'desc', per_page:15 }); | |
const prev = items.find(i => i.number !== currentNum); | |
if (!prev) { | |
core.info('No previous ManualβTest issue for author.'); | |
core.setOutput('prev_num',''); return; | |
} | |
core.info(`Previous issue is #${prev.number}`); | |
core.setOutput('prev_num', String(prev.number)); | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 5bΒ Read βEndΒ Dateβ from that previous issue (if any) | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Read End Date from previous issue | |
id: read_prev_end | |
if: steps.find_previous.outputs.prev_num != '' | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const {github,core} = global; | |
const prevNum = parseInt(core.getInput('prev_num'),10); | |
const {owner,repo} = github.context.repo; | |
const {data:prevIssue} = await github.rest.issues.get({ | |
owner, repo, issue_number: prevNum | |
}); | |
if (!prevIssue.node_id) { core.setOutput('prev_end',''); return; } | |
// walk project items β field βEndΒ Dateβ | |
const itemsQ = ` | |
query($id:ID!){ | |
node(id:$id){ ... on Issue { | |
projectItems(first:50){ nodes{ id } } } } }`; | |
const {node} = await github.graphql(itemsQ,{id:prevIssue.node_id}); | |
let found=''; | |
for (const it of node.projectItems.nodes){ | |
const fvQ=` | |
query($id:ID!){ | |
node(id:$id){ ... on ProjectV2Item { | |
fieldValues(first:50){ | |
nodes{ | |
__typename | |
... on ProjectV2ItemFieldDateValue{ | |
field{ ... on ProjectV2FieldCommon{ name id } } | |
date | |
} | |
} | |
} | |
}} | |
}`; | |
const {node:item} = await github.graphql(fvQ,{id:it.id}); | |
for (const f of item.fieldValues.nodes){ | |
if (f.__typename==='ProjectV2ItemFieldDateValue' && f.field.name==='End Date' && f.date){ | |
found=f.date; break; } | |
} | |
if (found) break; | |
} | |
core.setOutput('prev_end',found); | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# 5cΒ Update all projectβfields on the *current* issue | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
- name: Update fields on linked project item(s) | |
env: | |
NUMERIC_ID : ${{ steps.extract_id.outputs.numeric_test_id }} | |
PREV_END : ${{ steps.read_prev_end.outputs.prev_end }} | |
TEST_RESULT : ${{ steps.parse_body.outputs.test_result }} | |
OS_USED : ${{ steps.parse_body.outputs.os_used }} | |
LV_VERSION : ${{ steps.parse_body.outputs.labview_version }} | |
LV_BITNESS : ${{ steps.parse_body.outputs.labview_bitness }} | |
NOTES : ${{ steps.parse_body.outputs.notes }} | |
ESTIMATE_NUM : "" # placeholder β calculated elsewhere | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const {github,core,context} = global; | |
const issueNodeId = context.payload.issue.node_id; | |
if (!issueNodeId){ core.notice('No node_id β cannot touch project'); return; } | |
// pull all projectβitem IDs for this issue | |
const itemsQ = ` | |
query($id:ID!){ | |
node(id:$id){ ... on Issue { | |
projectItems(first:50){ nodes{ id project{ id title } } } } } | |
}`; | |
const {node} = await github.graphql(itemsQ,{id:issueNodeId}); | |
const items = node.projectItems.nodes; | |
if (!items.length){ core.notice('Issue not in any project'); return; } | |
// helper to read field values for an item | |
const readFields = async itemId => { | |
const q=` | |
query($id:ID!){ | |
node(id:$id){ ... 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 {node} = await github.graphql(q,{id:itemId}); | |
const m={}; | |
for (const v of node.fieldValues.nodes){ | |
const n=v.field?.name; if(!n)continue; | |
let cur = v.text ?? v.name ?? v.date ?? v.number ?? ''; | |
m[n]={id:v.field.id, type:v.__typename, current:cur}; | |
} | |
return m; | |
}; | |
// helper to write value | |
const write = async (proj,item,field,val) => { | |
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,field,val}); | |
}; | |
// data from env | |
const data = { | |
'TestID' : {type:'text', value:process.env.NUMERIC_ID}, | |
'LabVIEW Version' : {type:'text', value:process.env.LV_VERSION}, | |
'LabVIEW Bitness' : {type:'text', value:process.env.LV_BITNESS}, | |
'Operating System' : {type:'text', value:process.env.OS_USED}, | |
'Notes' : {type:'text', value:process.env.NOTES}, | |
'Estimate' : {type:'number', value:Number(process.env.ESTIMATE_NUM||0)}, | |
'Test Result' : {type:'single', value:process.env.TEST_RESULT}, | |
'End Date' : {type:'date', value:context.payload.issue.created_at}, | |
'Start Date' : {type:'date', value:process.env.PREV_END} | |
}; | |
for (const it of items){ | |
const fields = await readFields(it.id); | |
for (const [fname,def] of Object.entries(data)){ | |
if (!fields[fname]) continue; // field not in project | |
const cur = fields[fname].current || ''; | |
if (fname==='End Date' && cur) continue; // don't overwrite | |
if (fname==='Start Date' && cur) continue; // don't overwrite | |
if (fname==='Start Date' && !def.value) continue; // nothing to set | |
const valObj = ( | |
def.type==='text' ? {text:def.value} : | |
def.type==='number' ? {number:def.value} : | |
def.type==='date' ? {date:def.value} : | |
/*single*/ {singleSelectValue:def.value} | |
); | |
await write(it.project.id, it.id, fields[fname].id, valObj); | |
} | |
core.info(`β Updated item in project β${it.project.title}β`); | |
} | |
- name: Done | |
run: echo "Workflow completed β" |