[Manual Test] {{ test_id }} by {{ user_name }} #63
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 Processor | |
on: | |
issues: | |
types: [opened, edited] | |
jobs: | |
process-manual-test: | |
if: github.event.issue.title == 'Manual Test' | |
runs-on: ubuntu-24.04 | |
permissions: | |
contents: read # checkout | |
issues: write # comment / label if you ever need it | |
steps: | |
# ──────────────────────────────────────────────────────────────────── | |
- name: ⬇️ Checkout | |
uses: actions/checkout@v3 | |
# Parse the issue‑form body into discrete outputs | |
- name: 📝 Parse issue form | |
id: parse | |
uses: issue-ops/[email protected] | |
with: | |
body: ${{ github.event.issue.body }} | |
issue-form-template: manual-test-report.yml | |
workspace: ${{ github.workspace }} | |
# Promote parsed keys to env so subsequent steps can read them | |
- name: 🔧 Map parsed values to env | |
run: | | |
echo "TEST_ID=${{ steps.parse.outputs.test_id }}" >> $GITHUB_ENV | |
echo "ESTIMATE=${{ steps.parse.outputs.estimate }}" >> $GITHUB_ENV | |
echo "LV_VERSION=${{ steps.parse.outputs.labview_version_used }}" >> $GITHUB_ENV | |
echo "BITNESS=${{ steps.parse.outputs.labview_bitness }}" >> $GITHUB_ENV | |
echo "OS_USED=${{ steps.parse.outputs.operating_system }}" >> $GITHUB_ENV | |
echo "TEST_RESULT=${{ steps.parse.outputs.test_result }}" >> $GITHUB_ENV | |
echo "NOTES=${{ steps.parse.outputs.notes_or_screenshots_optional }}" >> $GITHUB_ENV | |
# ──────────────────────────────────────────────────────────────────── | |
- name: 🗂️ Update project fields | |
uses: actions/github-script@v6 | |
env: | |
ORG_LOGIN: ni # organisation login | |
PROJ_NUMBER: 29 # project number (from https://github.com/orgs/ni/projects/29) | |
TEST_ID: ${{ env.TEST_ID }} | |
ESTIMATE: ${{ env.ESTIMATE }} | |
LV_VERSION: ${{ env.LV_VERSION }} | |
BITNESS: ${{ env.BITNESS }} | |
OS_USED: ${{ env.OS_USED }} | |
TEST_RESULT: ${{ env.TEST_RESULT }} | |
NOTES: ${{ env.NOTES }} | |
with: | |
github-token: ${{ secrets.PROJECTS_PAT }} # PAT with project scope | |
script: | | |
/* ────────────────────────────────────────────────────────────── | |
GitHub‑Script: add the issue to the org project (if missing), | |
ensure all custom fields & options exist, then populate them. | |
Rich log output is kept for troubleshooting. | |
─────────────────────────────────────────────────────────── */ | |
const orgLogin = process.env.ORG_LOGIN; | |
const projectNumber = Number(process.env.PROJ_NUMBER); | |
const contentId = context.payload.issue.node_id; | |
const log = (m)=>core.info(` ${m}`); | |
const warn = (m)=>core.warning(` ${m}`); | |
// ── 1. Locate the project ──────────────────────────────────── | |
log(`🔍 Looking up Project #${projectNumber} in org “${orgLogin}”`); | |
const pjRes = await github.graphql(` | |
query ($org:String!,$num:Int!){ | |
organization(login:$org){ | |
projectV2(number:$num){ id title } | |
} | |
}`, { org:orgLogin, num:projectNumber }); | |
const project = pjRes.organization.projectV2; | |
if (!project) core.setFailed(`❌ Project #${projectNumber} not found`); | |
log(`➡️ Using project “${project.title}”`); | |
const projectId = project.id; | |
// ── 2. Ensure a project‑item exists for this issue ─────────── | |
async function ensureItem(){ | |
const r = await github.graphql(` | |
query ($pid:ID!){ | |
node(id:$pid){ | |
... on ProjectV2{ | |
items(first:100){ | |
nodes{ | |
id | |
content { ... on Issue { id } ... on PullRequest { id } } | |
} | |
} | |
} | |
} | |
}`, { pid:projectId }); | |
const hit = r.node.items.nodes.find(n => n.content?.id === contentId); | |
if (hit){ log(`ℹ️ Found project item ${hit.id}`); return hit.id; } | |
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 (or lazily create) field configurations ───────── | |
async function fetchFields(){ | |
const r = await github.graphql(` | |
query ($pid:ID!){ | |
node(id:$pid){ | |
... on ProjectV2{ | |
fields(first:100){ | |
nodes{ | |
__typename | |
... on ProjectV2TextField { id name } | |
... on ProjectV2NumberField { id name } | |
... on ProjectV2DateField { id name } | |
... on ProjectV2IterationField { id name } | |
... on ProjectV2SingleSelectField{ | |
id name options{ id name } | |
} | |
} | |
} | |
} | |
} | |
}`, { pid:projectId }); | |
return r.node.fields.nodes; | |
} | |
async function createField(name,type){ | |
log(`⚙️ Creating field “${name}” (${type})`); | |
const r = 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{ | |
__typename id name | |
... on ProjectV2SingleSelectField { options{ id name } } | |
} | |
} | |
}`, { pid:projectId, name, type }); | |
return r.createProjectV2Field.field; | |
} | |
async function ensureOption(field,optName){ | |
if (field.__typename!=='ProjectV2SingleSelectField') return null; | |
const existing = field.options.find(o => o.name === optName); | |
if (existing) return existing.id; | |
log(`⚙️ Adding option “${optName}”`); | |
const r = 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:optName }); | |
return r.createProjectV2SingleSelectFieldOption.option.id; | |
} | |
async function setField(itemId,field,val){ | |
let payload; | |
if (field.__typename==='ProjectV2SingleSelectField'){ | |
const optId = await ensureOption(field,val); | |
payload = { singleSelectOptionId: optId }; | |
} else { | |
payload = { text: val }; | |
} | |
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}” → “${val}”`); | |
} | |
// ── 4. Orchestrate ─────────────────────────────────────────── | |
const itemId = await ensureItem(); | |
let fields = await fetchFields(); | |
const desired = [ | |
{ 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 f of desired){ | |
if (!f.v){ warn(`Skipping “${f.n}” – empty`); continue; } | |
let fld = fields.find(x => x.name === f.n); | |
if (!fld){ fld = await createField(f.n,f.t); fields.push(fld); } | |
await setField(itemId,fld,f.v); | |
} | |
log('🎉 Done'); |