[Manual Test] {{ test_id }} by {{ user_name }} #42
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 (Issues → Project) | |
on: | |
issues: | |
types: [opened, edited] | |
# the job writes to issues & projects, so default perms are tightened | |
permissions: | |
contents: read | |
issues: write | |
jobs: | |
process-manual-test: | |
runs-on: ubuntu-latest | |
steps: | |
# ─────────────────────────────── 1. Checkout ────────────────────────────── | |
- name: Checkout | |
uses: actions/checkout@v3 | |
# ───────────────────────────── 2. Parse form ────────────────────────────── | |
- name: Parse issue form | |
id: parse # ← identifier used later | |
uses: issue-ops/[email protected] | |
with: | |
body: ${{ github.event.issue.body }} | |
issue-form-template: manual-test-report.yml | |
workspace: ${{ github.workspace }} | |
# ─────────────────────────── 3. Build field set ─────────────────────────── | |
- name: Build field set | |
id: fields | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.PROJECTS_TOKEN }} | |
script: | | |
const fs = require('fs'); | |
const path = require('path'); | |
// ——— helper ——— | |
function pick(obj, key){ | |
const v = obj[key]; | |
return Array.isArray(v) ? v[0] ?? '' : (v ?? ''); | |
} | |
// ---------- load parsed JSON from previous step ---------- | |
const form = JSON.parse(process.env.FORM_JSON); | |
const alias = pick(form,'select_a_test'); | |
const lvVersion = pick(form,'labview_version_used'); | |
const lvBitness = pick(form,'labview_bitness'); | |
const osUsed = pick(form,'operating_system'); | |
const testResult = pick(form,'test_result'); | |
const notes = form.notes_or_screenshots_optional ?? ''; | |
if (!alias || !lvVersion || !lvBitness || !osUsed || !testResult) | |
core.setFailed('Required form value missing.'); | |
if (!['Passed','Failed','Needs Review'].includes(testResult)) | |
core.setFailed(`Invalid Test Result: ${testResult}`); | |
// ---------- read markdown table inside template ---------- | |
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)) | |
core.setFailed('manual-test-report.yml not found in repository.'); | |
const lines = fs.readFileSync(tplPath,'utf8') | |
.split('\n') | |
.map(l=>l.trimStart()) | |
.filter(l=>l.startsWith('|')); // only table rows | |
const key = alias.replace(/_/g,' ').toLowerCase(); | |
let numeric='', estimate=''; | |
for (const r of lines){ | |
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(); | |
const m = cols[3].match(/\/([0-9]{7})\.md\)?$/); | |
numeric = m?.[1] ?? ''; | |
break; | |
} | |
if (!numeric) core.setFailed(`No table row found for “${alias}”`); | |
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); | |
env: | |
FORM_JSON: ${{ steps.parse.outputs.form-json }} | |
# ────────── 4. Get previous test’s End Date (step id: prev) – token fixed ── | |
- name: Get previous test's end date | |
id: prev | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.PROJECTS_TOKEN }} # ← was missing | |
script: | | |
const {owner,repo} = context.repo; | |
const curIssue = +process.env.CUR_NUMBER; | |
const author = context.payload.issue.user.login; | |
const queryStr = `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{ | |
... on ProjectV2ItemFieldDateValue{ | |
date | |
field{ name } | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
}`, {q: queryStr}); | |
let prevEnd=''; | |
for (const n of res.search.nodes){ | |
if (n.number === curIssue) continue; | |
for (const fv of n.projectItems.nodes.flatMap(x=>x.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. Push data to the project ─────────────────────── | |
- name: Update project fields | |
uses: actions/github-script@v6 | |
with: | |
github-token: ${{ secrets.PROJECTS_TOKEN }} | |
script: | | |
const {owner,repo} = context.repo; | |
const issueNode = context.payload.issue.node_id; | |
const createdDate = context.payload.issue.created_at.substring(0,19)+'Z'; | |
// ---------- fetch 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 when 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 every 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{ id name } | |
} | |
... on ProjectV2ItemFieldDateValue{ | |
date | |
field{ id name } | |
} | |
... on ProjectV2ItemFieldSingleSelectValue{ | |
id | |
name | |
field{ id name } | |
} | |
} | |
} | |
} | |
} | |
}`, {id: it.id}); | |
// Build a quick lookup map: field‑name → {fieldId,type,node} | |
const map = {}; | |
for (const n of fv.node.fieldValues.nodes){ | |
if (!n.field) continue; // safeguard | |
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 — refresh if previous 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 pairs = [ | |
['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 pairs){ | |
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: | |
PREV: ${{ steps.prev.outputs.prev_end }} | |
NUMID: ${{ steps.fields.outputs.numeric_test_id }} | |
EST: ${{ steps.fields.outputs.estimate_text }} | |
OS: ${{ steps.fields.outputs.os_used }} | |
LV: ${{ steps.fields.outputs.labview_version }} | |
BIT: ${{ steps.fields.outputs.labview_bitness }} | |
NOTES: ${{ steps.fields.outputs.notes }} |