Skip to content

[Manual Test] {{ test_id }} by {{ user_name }} #24

[Manual Test] {{ test_id }} by {{ user_name }}

[Manual Test] {{ test_id }} by {{ user_name }} #24

Workflow file for this run

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."