Skip to content

[Manual Test] {{ test_id }} by {{ user_name }} #33

[Manual Test] {{ test_id }} by {{ user_name }}

[Manual Test] {{ test_id }} by {{ user_name }} #33

Workflow file for this run

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

Check failure on line 10 in .github/workflows/manual-test.yml

View workflow run for this annotation

GitHub Actions / Manual‑Test Issue Processing

Invalid workflow file

The workflow is not valid. .github/workflows/manual-test.yml (Line: 10, Col: 3): Unexpected value 'projects'
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."