[Manual Test] {{ test_id }} by {{ user_name }} #56
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 Report | |
on: | |
issues: | |
types: [opened, edited] # run on new issues and on later edits | |
permissions: | |
contents: read | |
issues: write | |
pull-requests: write | |
jobs: | |
process-manual-test: | |
if: contains(github.event.issue.title, '[Manual Test]') | |
runs-on: ubuntu-latest | |
steps: | |
#-------------------------------------------------------------------- | |
# 1) Check out repository so the issue‑form template is available | |
#-------------------------------------------------------------------- | |
- name: Checkout repository | |
uses: actions/checkout@v3 | |
with: { fetch-depth: 1 } | |
#-------------------------------------------------------------------- | |
# 2) Parse the issue‑form submission | |
#-------------------------------------------------------------------- | |
- name: Parse issue form | |
id: parse | |
uses: issue-ops/[email protected] | |
with: | |
body: ${{ github.event.issue.body }} | |
issue-form-template: manual-test-report.yml # .github/ISSUE_TEMPLATE/ | |
workspace: ${{ github.workspace }} | |
#-------------------------------------------------------------------- | |
# 3) Gather all data for the project fields | |
#-------------------------------------------------------------------- | |
- name: Build field set | |
id: build | |
run: | | |
{ | |
echo "test_id=${{ steps.parse.outputs.numeric_test_id }}" | |
echo "estimate=${{ steps.parse.outputs.estimate_text }}" | |
echo "lv_version=${{ steps.parse.outputs.labview_version }}" | |
echo "bitness=${{ steps.parse.outputs.labview_bitness }}" | |
echo "os_used=${{ steps.parse.outputs.os_used }}" | |
echo "test_result=${{ steps.parse.outputs.test_result }}" | |
echo "notes=${{ steps.parse.outputs.notes }}" | |
} >> "$GITHUB_OUTPUT" | |
#-------------------------------------------------------------------- | |
# 4) Update / create project fields in Org‑level Project #29 | |
#-------------------------------------------------------------------- | |
- name: Update project fields | |
uses: actions/github-script@v6 | |
env: | |
ORG_LOGIN: ni # << organisation login | |
PROJ_NUMBER: 29 # << project number from the URL | |
TEST_ID: ${{ steps.build.outputs.test_id }} | |
ESTIMATE: ${{ steps.build.outputs.estimate }} | |
LV_VERSION: ${{ steps.build.outputs.lv_version }} | |
BITNESS: ${{ steps.build.outputs.bitness }} | |
OS_USED: ${{ steps.build.outputs.os_used }} | |
TEST_RESULT: ${{ steps.build.outputs.test_result }} | |
NOTES: ${{ steps.build.outputs.notes }} | |
with: | |
github-token: ${{ secrets.PROJECTS_PAT }} # PAT with “Projects RW” & “Issues R” | |
script: | | |
const orgLogin = process.env.ORG_LOGIN; | |
const projectNumber= parseInt(process.env.PROJ_NUMBER, 10); | |
const contentId = context.payload.issue.node_id; | |
//---------------------------------------------------------------- | |
// Helper logging | |
//---------------------------------------------------------------- | |
const log = (msg) => core.info(` ${msg}`); | |
const warn = (msg) => core.warning(` ${msg}`); | |
//---------------------------------------------------------------- | |
// 1) Resolve the project node‑ID dynamically | |
//---------------------------------------------------------------- | |
log(`🔍 Resolving Project #${projectNumber} in org “${orgLogin}”`); | |
const projRes = await github.graphql(` | |
query ($org:String!, $num:Int!){ | |
organization(login:$org){ | |
projectV2(number:$num){ id title } | |
} | |
}`, { org: orgLogin, num: projectNumber }); | |
const project = projRes.organization.projectV2; | |
if (!project) core.setFailed(`Project #${projectNumber} not found in “${orgLogin}”.`); | |
const projectId = project.id; | |
log(`➡️ Using project “${project.title}” (ID: ${projectId})`); | |
//---------------------------------------------------------------- | |
// 2) Ensure a project item exists for this issue | |
//---------------------------------------------------------------- | |
async function ensureProjectItem () { | |
const itemsRes = await github.graphql(` | |
query ($pid:ID!, $after:String){ | |
node(id:$pid){ | |
... on ProjectV2 { | |
items(first:100, after:$after){ | |
nodes { id content { ... on Issue { id } } } | |
pageInfo { hasNextPage endCursor } | |
} | |
} | |
} | |
}`, { pid: projectId }); | |
let itemId = | |
itemsRes.node.items.nodes.find(n => n.content?.id === contentId)?.id; | |
// If not found (or >100 items), keep paginating | |
let cursor = itemsRes.node.items.pageInfo.endCursor; | |
let hasMore= itemsRes.node.items.pageInfo.hasNextPage; | |
while (!itemId && hasMore) { | |
const moreRes = await github.graphql(/* GraphQL */` | |
query ($pid:ID!, $after:String){ | |
node(id:$pid){ | |
... on ProjectV2 { | |
items(first:100, after:$after){ | |
nodes { id content { ... on Issue { id } } } | |
pageInfo { hasNextPage endCursor } | |
} | |
} | |
} | |
}`, { pid: projectId, after: cursor }); | |
itemId = | |
moreRes.node.items.nodes.find(n => n.content?.id === contentId)?.id; | |
cursor = moreRes.node.items.pageInfo.endCursor; | |
hasMore = moreRes.node.items.pageInfo.hasNextPage; | |
} | |
if (itemId) { | |
log(`ℹ️ Found existing project item ${itemId}`); | |
return itemId; | |
} | |
log('➕ Adding issue to project…'); | |
const add = await github.graphql(` | |
mutation ($pid:ID!, $cid:ID!){ | |
addProjectV2ItemById(input:{ projectId:$pid, contentId:$cid }){ | |
item { id } | |
} | |
}`, { pid: projectId, cid: contentId }); | |
return add.addProjectV2ItemById.item.id; | |
} | |
//---------------------------------------------------------------- | |
// 3) Fetch project fields – create missing ones | |
//---------------------------------------------------------------- | |
async function fetchFields () { | |
const res = await github.graphql(` | |
query ($pid:ID!){ | |
node(id:$pid){ | |
... on ProjectV2 { | |
fields(first:100){ | |
nodes { | |
id name dataType | |
... on ProjectV2SingleSelectField { options { id name } } | |
} | |
} | |
} | |
} | |
}`, { pid: projectId }); | |
return res.node.fields.nodes; | |
} | |
async function createField (name, type) { | |
log(`⚙️ Creating field “${name}”`); | |
const res = await github.graphql(` | |
mutation ($pid:ID!, $name:String!, $type:ProjectV2CustomFieldDataType!){ | |
createProjectV2Field(input:{ | |
projectId:$pid, name:$name, dataType:$type, | |
${type === 'SINGLE_SELECT' | |
? 'singleSelectOptions:[{name:"Passed"},{name:"Failed"},{name:"Needs Review"}]' | |
: ''} | |
}){ | |
field { | |
id name dataType | |
... on ProjectV2SingleSelectField { options { id name } } | |
} | |
} | |
}`, { pid: projectId, name, type }); | |
return res.createProjectV2Field.field; | |
} | |
async function ensureOption (field, optionName){ | |
const opt = field.options.find(o => o.name === optionName); | |
if (opt) return opt.id; | |
log(`⚙️ Adding option “${optionName}” to “${field.name}”`); | |
const res = await github.graphql(` | |
mutation ($pid:ID!, $fid:ID!, $name:String!){ | |
createProjectV2SingleSelectFieldOption(input:{ | |
projectId:$pid, fieldId:$fid, name:$name | |
}){ | |
option { id } | |
} | |
}`, { pid: projectId, fid: field.id, name: optionName }); | |
return res.createProjectV2SingleSelectFieldOption.option.id; | |
} | |
async function setField (itemId, field, value){ | |
let payload; | |
if (field.dataType === 'SINGLE_SELECT'){ | |
const optId = await ensureOption(field, value); | |
payload = { singleSelectOptionId: optId }; | |
} else { | |
payload = { text: value }; | |
} | |
await github.graphql(` | |
mutation ($pid:ID!, $iid:ID!, $fid:ID!, $val:ProjectV2FieldValue!){ | |
updateProjectV2ItemFieldValue(input:{ | |
projectId:$pid, itemId:$iid, fieldId:$fid, value:$val | |
}) { projectV2Item { id } } | |
}`, { pid: projectId, iid: itemId, fid: field.id, val: payload }); | |
log(`✅ “${field.name}” → “${value}”`); | |
} | |
//---------------------------------------------------------------- | |
// 4) Main execution | |
//---------------------------------------------------------------- | |
const itemId = await ensureProjectItem(); | |
const fields = await fetchFields(); | |
const entries = [ | |
{ n:'TestID', v:process.env.TEST_ID, t:'TEXT' }, | |
{ n:'Estimate', v:process.env.ESTIMATE, t:'TEXT' }, | |
{ n:'Operating System', v:process.env.OS_USED, t:'TEXT' }, | |
{ n:'LabVIEW Version', v:process.env.LV_VERSION, t:'TEXT' }, | |
{ n:'LabVIEW Bitness', v:process.env.BITNESS, t:'TEXT' }, | |
{ n:'Notes', v:process.env.NOTES, t:'TEXT' }, | |
{ n:'Test Result', v:process.env.TEST_RESULT, t:'SINGLE_SELECT' } | |
]; | |
log('✏️ Updating project fields'); | |
for (const {n,v,t} of entries){ | |
if (!v){ warn(`Skipping “${n}” – empty value`); continue; } | |
let fld = fields.find(f => f.name === n); | |
if (!fld){ | |
fld = await createField(n, t); | |
fields.push(fld); | |
} | |
await setField(itemId, fld, v); | |
} | |
log('🎉 Project update complete'); |