[Manual Test] {{ test_id }} by {{ user_name }} #33
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 | ||
# -------------------------------------------------- | ||
# ➜ grant the GITHUB_TOKEN access to Projects V2 ⬅ | ||
# -------------------------------------------------- | ||
permissions: | ||
contents: read # checkout | ||
issues: write # comment / label | ||
pull-requests: write | ||
projects: write # ←‑‑ new, lets GraphQL use Projects V2 | ||
repository-projects: write # (older token name, harmless to keep) | ||
on: | ||
issues: | ||
types: [opened, edited, labeled] | ||
# ------------------------------------------------------ | ||
# Ensure only one run per issue (newer runs cancel older) | ||
# ------------------------------------------------------ | ||
concurrency: | ||
group: manual-test-${{ github.event.issue.number }} | ||
cancel-in-progress: true | ||
jobs: | ||
process-manual-test: | ||
# Run only if the manual‑test label is present | ||
if: contains(toJson(github.event.issue.labels), 'manual-test') | ||
runs-on: ubuntu-latest | ||
steps: | ||
# ------------------------------------------------------------------ | ||
# 1) Warn if the issue title does not start with “[Manual Test] …” | ||
# ------------------------------------------------------------------ | ||
- 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]'" | ||
# 2) Check out the repository (needed for template file & GraphQL IDs) | ||
- uses: actions/checkout@v3 | ||
# ------------------------------------------------------------------ | ||
# 3) Parse the issue form – returns a JSON structure in steps.parse | ||
# ------------------------------------------------------------------ | ||
- 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 | ||
# ------------------------------------------------------------------ | ||
# 4) Validate fields & extract table row from the issue‑template file | ||
# ------------------------------------------------------------------ | ||
- name: Validate & extract table data | ||
id: process | ||
uses: actions/github-script@v6 | ||
env: | ||
FORM_JSON: ${{ steps.parse.outputs.json }} | ||
with: | ||
script: | | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
// ---------- map Issue‑form fields ---------- | ||
const form = JSON.parse(process.env.FORM_JSON); | ||
const alias = (form.select_a_test ?? [])[0] ?? ''; | ||
const lvVersion = (form.labview_version_used ?? [])[0] ?? ''; | ||
const lvBitness = (form.labview_bitness ?? [])[0] ?? ''; | ||
const osUsed = (form.operating_system ?? [])[0] ?? ''; | ||
const testResult = (form.test_result ?? [])[0] ?? ''; | ||
const notes = form.notes_or_screenshots_optional ?? ''; | ||
function fail(msg){ core.setFailed(msg); return; } | ||
if (!alias) fail('Missing Select‑a‑Test value'); | ||
if (!lvVersion) fail('Missing LabVIEW Version'); | ||
if (!lvBitness) fail('Missing LabVIEW Bitness'); | ||
if (!osUsed) fail('Missing Operating System'); | ||
if (!testResult) fail('Missing Test Result'); | ||
const valid = ['Passed','Failed','Needs Review']; | ||
if (!valid.includes(testResult)) | ||
fail(`Invalid Test Result: ${testResult}`); | ||
// ---------- read markdown table from the template file ---------- | ||
const root = process.env.GITHUB_WORKSPACE; | ||
let tplPath = path.join(root, '.github', 'ISSUE_TEMPLATE', 'manual-test-report.yml'); | ||
if (!fs.existsSync(tplPath)) | ||
tplPath = path.join(root, 'manual-test-report.yml'); // fallback | ||
if (!fs.existsSync(tplPath)) | ||
fail('Cannot locate manual-test-report.yml template file.'); | ||
const templateText = fs.readFileSync(tplPath, 'utf8'); | ||
const rows = templateText | ||
.split('\n') | ||
.map(l => l.trimStart()) | ||
.filter(l => l.startsWith('|')); // only table rows | ||
if (rows.length === 0) | ||
fail('No markdown table found inside the issue template.'); | ||
// ---------- find the row matching the alias ---------- | ||
const key = alias.replace(/_/g,' ').toLowerCase(); | ||
let numeric='', estimate=''; | ||
for (const r of rows){ | ||
const cols = r.split('|').map(c => c.trim()); | ||
if (cols.length < 4) continue; | ||
if (cols[1].toLowerCase() !== key) continue; | ||
estimate = cols[2].replace(/\*/g,'').trim(); | ||
if (!estimate) fail(`No Est. Time for “${cols[1]}”`); | ||
const m = cols[3].match(/\/([0-9]{7})\.md\)?$/); | ||
if (!m) fail('Link column does not contain 7‑digit .md filename'); | ||
numeric = m[1]; | ||
break; | ||
} | ||
if (!numeric) fail(`No matching row for alias ${alias}`); | ||
// ---------- expose outputs ---------- | ||
core.setOutput('numeric_test_id', numeric); | ||
core.setOutput('estimate_text', estimate); | ||
core.setOutput('labview_version', lvVersion); | ||
core.setOutput('labview_bitness', lvBitness); | ||
core.setOutput('os_used', osUsed); | ||
core.setOutput('test_result', testResult); | ||
core.setOutput('notes', notes); | ||
core.info(`Parsed table row → ID=${numeric}, Est=${estimate}`); | ||
# ------------------------------------------------------------------ | ||
# 5) Locate the author’s previous manual‑test issue to get End Date | ||
# ------------------------------------------------------------------ | ||
- 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{ | ||
fieldValues(first:50){ | ||
nodes{ | ||
__typename | ||
... on ProjectV2ItemFieldDateValue{ | ||
date | ||
field{ ... on ProjectV2FieldCommon{ name } } | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}`, {q: query}); | ||
let prevEnd=''; | ||
for (const n of res.search.nodes){ | ||
if (n.number === cur) continue; | ||
for (const fv of n.projectItems.nodes.flatMap(it=>it.fieldValues.nodes)){ | ||
if (fv.field?.name === 'End Date' && fv.date){ | ||
prevEnd = fv.date; break; | ||
} | ||
} | ||
if (prevEnd) break; | ||
} | ||
core.setOutput('prev_end', prevEnd); | ||
core.info(prevEnd ? `Previous End Date: ${prevEnd}` : 'No previous End Date'); | ||
# ------------------------------------------------------------------ | ||
# 6) Ensure the issue has a Project V2 item & update all fields | ||
# ------------------------------------------------------------------ | ||
- name: Create / update project fields | ||
uses: actions/github-script@v6 | ||
env: | ||
NUMID: ${{ 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 createdDate = context.payload.issue.created_at.substring(0,19)+'Z'; | ||
// ---------- get existing project 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; | ||
// ---------- create project item if none ---------- | ||
if (items.length === 0){ | ||
const pl = 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 = pl.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 to update one field ---------- | ||
async function setField(pid, iid, fid, value){ | ||
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: value}); | ||
} | ||
// ---------- iterate each project item ---------- | ||
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{ | ||
text | ||
field{ ... on ProjectV2FieldCommon{ id name } } | ||
} | ||
... on ProjectV2ItemFieldDateValue{ | ||
date | ||
field{ ... on ProjectV2FieldCommon{ id name } } | ||
} | ||
... on ProjectV2ItemFieldSingleSelectValue{ | ||
id | ||
name | ||
field{ ... on ProjectV2FieldCommon{ id name } } | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}`, {id: it.id}); | ||
const map = {}; | ||
for (const n of fv.node.fieldValues.nodes){ | ||
map[n.field.name] = { | ||
fieldId : n.field.id, | ||
type : n.__typename, | ||
node : n | ||
}; | ||
} | ||
// End Date (write once) | ||
if (map['End Date'] && !map['End Date'].node.date){ | ||
await setField(it.project.id, it.id, map['End Date'].fieldId, | ||
{date: createdDate}); | ||
} | ||
// Start Date (every run if prev exists) | ||
if (process.env.PREV && map['Start Date']){ | ||
await setField(it.project.id, it.id, map['Start Date'].fieldId, | ||
{date: process.env.PREV}); | ||
} | ||
// plain text fields | ||
const textPairs = [ | ||
['TestID', process.env.NUMID], | ||
['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 [fname, val] of textPairs){ | ||
if (map[fname]) | ||
await setField(it.project.id, it.id, map[fname].fieldId, {text: val}); | ||
} | ||
// Test Result (single‑select) | ||
if (map['Test Result'] && map['Test Result'].type === 'ProjectV2ItemFieldSingleSelectValue'){ | ||
await setField(it.project.id, it.id, map['Test Result'].fieldId, { | ||
singleSelectOptionId: map['Test Result'].node.id | ||
}); | ||
} | ||
} | ||
# ------------------------------------------------------------------ | ||
# 7) Append/update a docs/test_reports.json log in the repo | ||
# ------------------------------------------------------------------ | ||
- name: Archive test report | ||
uses: actions/github-script@v6 | ||
env: | ||
FORM_JSON: ${{ 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; // ignore file‑not‑found | ||
} | ||
const issueNum = context.payload.issue.number; | ||
data = data.filter(r => r.issue_number !== issueNum); | ||
const f = JSON.parse(process.env.FORM_JSON); | ||
data.push({ | ||
issue_number: issueNum, | ||
test_id: process.env.NUMID, | ||
estimate: process.env.EST, | ||
labview_version: (f.labview_version_used ?? [])[0] ?? '', | ||
labview_bitness: (f.labview_bitness ?? [])[0] ?? '', | ||
os_used: (f.operating_system ?? [])[0] ?? '', | ||
test_result: (f.test_result ?? [])[0] ?? '', | ||
notes: f.notes_or_screenshots_optional ?? '', | ||
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') | ||
}); | ||
# ------------------------------------------------------------------ | ||
# 8) Optionally assign the issue back to the reporter | ||
# ------------------------------------------------------------------ | ||
- name: Auto‑assign issue to reporter | ||
uses: kentaro-m/[email protected] | ||
with: | ||
repo-token: ${{ secrets.GITHUB_TOKEN }} | ||
assignees: author | ||
# ------------------------------------------------------------------ | ||
# 9) Done! | ||
# ------------------------------------------------------------------ | ||
- run: echo "✅ Done – Manual‑Test workflow completed successfully." |