[Manual Test] {{ test_id }} by {{ user_name }} #36
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] | |
jobs: | |
process-manual-test: | |
runs-on: ubuntu-latest | |
#------------------------------------------------------------- | |
# The PAT below must have at least the classic scopes: | |
# repo , project | |
# and be saved in the repository secrets as PROJECTS_PAT. | |
#------------------------------------------------------------- | |
permissions: | |
contents: read # checkout / metadata | |
issues: write # we comment / label | |
pull-requests: write # auto‑assign step | |
repository-projects: write # still required by the API, harmless with PAT | |
steps: | |
# 1. checkout so that we can read the issue‑template markdown table | |
- name: Checkout repository | |
uses: actions/checkout@v3 | |
# 2. parse the issue‑form body into JSON (issue‑ops/parser action) | |
- name: Parse issue‑form body | |
id: parse # <-- «id: parse» required for later references | |
uses: issue-ops/[email protected] | |
with: | |
body: ${{ github.event.issue.body }} | |
issue-form-template: manual-test-report.yml | |
workspace: ${{ github.workspace }} | |
# 3. validate fields + read the markdown mapping table | |
- name: Validate & extract table data | |
id: process | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.PROJECTS_PAT }} # ← PAT instead of GITHUB_TOKEN | |
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'); | |
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}`); | |
env: | |
# Use the correct output name from the parser action | |
FORM_JSON: ${{ steps.parse.outputs.json }} | |
# 4. locate the previous manual‑test issue from the same author | |
- name: Locate previous manual‑test issue | |
id: previous | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.PROJECTS_PAT }} # ← PAT | |
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'); | |
env: | |
CUR_NUMBER: ${{ github.event.issue.number }} | |
# 5. create / update the project item and its fields | |
- name: Create or update project fields | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.PROJECTS_PAT }} # ← PAT | |
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 | |
}); | |
} | |
} | |
env: | |
NUMID: ${{ steps.process.outputs.numeric_test_id }} | |
EST: ${{ steps.process.outputs.estimate_text }} | |
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 }} | |
# 6. optionally auto‑assign reviewers / triagers | |
- name: Auto‑assign issue handlers | |
if: success() | |
uses: kentaro-m/[email protected] |