Skip to content

Commit 95108fe

Browse files
committed
ci: fix hashFiles expression syntax in workflow
1 parent 8c9fb6b commit 95108fe

File tree

6 files changed

+276
-14
lines changed

6 files changed

+276
-14
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ jobs:
2222
swift --version || echo 'swift not installed'
2323
python --version
2424
- name: Build
25-
if: hashFiles('Package.swift') != ''
25+
if: ${{ hashFiles('Package.swift') != '' }}
2626
run: swift build -v
2727
- name: Test
28-
if: hashFiles('Package.swift') != ''
28+
if: ${{ hashFiles('Package.swift') != '' }}
2929
run: swift test -v
3030
- name: Validate YAML configs
3131
run: |
@@ -45,9 +45,14 @@ jobs:
4545
LOC=$(jq '.SUM.code' loc.json || echo 0)
4646
mkdir -p badges
4747
echo "<svg xmlns='http://www.w3.org/2000/svg' width='150' height='20'><rect rx='3' width='150' height='20' fill='#555'/><rect rx='3' x='60' width='90' height='20' fill='#007ec6'/><text x='30' y='14' fill='#fff' font-family='Verdana' font-size='11'>loc</text><text x='105' y='14' fill='#fff' font-family='Verdana' font-size='11'>${LOC}</text></svg>" > badges/loc.svg
48+
# If README previously used shields placeholder, update it.
4849
if grep -q "loc-dynamic" README.md; then
4950
sed -i '' "s|loc-dynamic-lightblue|loc-${LOC}-blue|" README.md || true
5051
fi
52+
# If README still has an old shields numeric badge, optionally switch to local SVG once.
53+
if grep -q "https://img.shields.io/badge/loc-" README.md; then
54+
perl -i -pe 's|!\[Lines of Code\]\(https://img.shields.io/badge/loc-[0-9]+-blue\)|![Lines of Code](badges/loc.svg)|' README.md || true
55+
fi
5156
git config user.name 'github-actions'
5257
git config user.email '[email protected]'
5358
git add badges/loc.svg README.md || true
@@ -64,7 +69,7 @@ jobs:
6469
folder-path: '.'
6570
swift-lint:
6671
runs-on: macos-14
67-
if: hashFiles('Package.swift') != ''
72+
if: ${{ hashFiles('Package.swift') != '' }}
6873
steps:
6974
- uses: actions/checkout@v4
7075
- name: Install SwiftFormat (placeholder)

.github/workflows/project-sync.yml

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
name: Project Sync
2+
3+
on:
4+
push:
5+
branches: [ main, '**' ]
6+
pull_request:
7+
types: [opened, reopened, synchronize]
8+
issues:
9+
types: [opened]
10+
11+
permissions:
12+
contents: read
13+
issues: write
14+
pull-requests: read
15+
projects: write
16+
17+
env:
18+
# Set these to match your environment.
19+
GITHUB_USER: pynip
20+
# Replace with the numeric project number for "Virtualization Studio ARM macOS"
21+
PROJECT_NUMBER: 3 # <-- UPDATE this after creating the project
22+
# Status names expected in the project's single-select Status field
23+
STATUS_TODO_NAME: "To do"
24+
STATUS_INPROGRESS_NAME: "In Progress"
25+
STATUS_DONE_NAME: "Done"
26+
27+
jobs:
28+
add-new-issues:
29+
name: Add newly opened issues to project (To do)
30+
if: github.event_name == 'issues'
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Add to project
34+
uses: actions/[email protected]
35+
with:
36+
project-url: https://github.com/users/${{ env.GITHUB_USER }}/projects/${{ env.PROJECT_NUMBER }}
37+
github-token: ${{ secrets.GITHUB_TOKEN }}
38+
39+
sync-from-push:
40+
name: Sync referenced issues from push commits
41+
if: github.event_name == 'push'
42+
runs-on: ubuntu-latest
43+
steps:
44+
- name: Extract issue numbers from commit messages
45+
id: extract
46+
run: |
47+
issues=$(echo "${{ toJson(github.event.commits) }}" | jq -r '.[].message' | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true)
48+
if [ -n "$issues" ]; then
49+
echo "issues=$issues" >> $GITHUB_OUTPUT
50+
else
51+
echo "issues=" >> $GITHUB_OUTPUT
52+
fi
53+
- name: Add referenced issues to project (if any)
54+
if: steps.extract.outputs.issues != ''
55+
uses: actions/[email protected]
56+
with:
57+
project-url: https://github.com/users/${{ env.GITHUB_USER }}/projects/${{ env.PROJECT_NUMBER }}
58+
github-token: ${{ secrets.GITHUB_TOKEN }}
59+
content-id: ${{ steps.extract.outputs.issues }}
60+
continue-on-error: true
61+
- name: Prepare status update script
62+
if: steps.extract.outputs.issues != ''
63+
uses: actions/setup-node@v4
64+
with:
65+
node-version: '20'
66+
- name: Update status for closed issues (Done)
67+
if: steps.extract.outputs.issues != ''
68+
run: |
69+
sudo apt-get update && sudo apt-get install -y jq
70+
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
71+
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
72+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
73+
sudo apt-get update && sudo apt-get install gh -y
74+
gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}"
75+
LOGIN=${{ env.GITHUB_USER }}
76+
PROJECT_NUM=${{ env.PROJECT_NUMBER }}
77+
ISSUES="${{ steps.extract.outputs.issues }}"
78+
# Fetch project + status field metadata
79+
projectQuery='query($login:String!,$number:Int!){ user(login:$login){ projectV2(number:$number){ id fields(first:50){ nodes { __typename ... on ProjectV2SingleSelectField { id name options { id name } } } } items(first:200){ nodes { id content { __typename ... on Issue { number id state } } } } } } }'
80+
resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
81+
projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id')
82+
statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id')
83+
optDone=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_DONE_NAME}"'") | .id')
84+
optInProg=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_INPROGRESS_NAME}"'") | .id')
85+
for issue in $ISSUES; do
86+
state=$(gh api repos/${{ github.repository }}/issues/$issue --jq '.state')
87+
# Find item id in project
88+
itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
89+
if [ -z "$itemId" ]; then
90+
# Attempt to add
91+
addMutation='mutation($project:ID!,$issue:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$issue}){ item { id } } }'
92+
issueNode=$(gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!){ repository(owner:$owner,name:$repo){ issue(number:$num){ id } } }' -F owner='${{ github.repository_owner }}' -F repo='${{ github.event.repository.name }}' -F num=$issue --jq '.data.repository.issue.id')
93+
gh api graphql -f query="$addMutation" -f project=$projectId -f issue=$issueNode >/dev/null || true
94+
# Refresh project items for new item
95+
resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
96+
itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
97+
fi
98+
if [ "$state" = "closed" ]; then
99+
# Set Done
100+
updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
101+
gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optDone >/dev/null || true
102+
else
103+
# Mark In Progress when there is at least one commit referencing it
104+
updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
105+
gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optInProg >/dev/null || true
106+
fi
107+
done
108+
109+
sync-from-pr:
110+
name: Sync referenced issues from PR
111+
if: github.event_name == 'pull_request'
112+
runs-on: ubuntu-latest
113+
steps:
114+
- name: Gather referenced issues
115+
id: refs
116+
run: |
117+
body="${{ github.event.pull_request.body }}\n${{ github.event.pull_request.title }}"
118+
issues=$(echo "$body" | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true)
119+
echo "issues=$issues" >> $GITHUB_OUTPUT
120+
- name: Install gh + jq
121+
if: steps.refs.outputs.issues != ''
122+
run: |
123+
sudo apt-get update && sudo apt-get install -y jq
124+
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
125+
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
126+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
127+
sudo apt-get update && sudo apt-get install gh -y
128+
- name: Update project status -> In Progress
129+
if: steps.refs.outputs.issues != ''
130+
env:
131+
ISSUES: ${{ steps.refs.outputs.issues }}
132+
run: |
133+
gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}"
134+
LOGIN=${{ env.GITHUB_USER }}
135+
PROJECT_NUM=${{ env.PROJECT_NUMBER }}
136+
projectQuery='query($login:String!,$number:Int!){ user(login:$login){ projectV2(number:$number){ id fields(first:50){ nodes { __typename ... on ProjectV2SingleSelectField { id name options { id name } } } } items(first:200){ nodes { id content { __typename ... on Issue { number id state } } } } } } }'
137+
resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
138+
projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id')
139+
statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id')
140+
optInProg=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_INPROGRESS_NAME}"'") | .id')
141+
updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
142+
for issue in $ISSUES; do
143+
itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
144+
if [ -z "$itemId" ]; then
145+
# Add issue first
146+
issueNode=$(gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!){ repository(owner:$owner,name:$repo){ issue(number:$num){ id } } }' -F owner='${{ github.repository_owner }}' -F repo='${{ github.event.repository.name }}' -F num=$issue --jq '.data.repository.issue.id')
147+
addMutation='mutation($project:ID!,$issue:ID!){ addProjectV2ItemById(input:{projectId:$project contentId:$issue}){ item { id } } }'
148+
gh api graphql -f query="$addMutation" -f project=$projectId -f issue=$issueNode >/dev/null || true
149+
# Refresh
150+
resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
151+
itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
152+
fi
153+
gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optInProg >/dev/null || true
154+
done
155+
156+
finalize-merged:
157+
name: Mark closed issues Done after merge
158+
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true
159+
runs-on: ubuntu-latest
160+
steps:
161+
- name: Extract issues from PR body/title
162+
id: issues
163+
run: |
164+
body="${{ github.event.pull_request.body }}\n${{ github.event.pull_request.title }}"
165+
issues=$(echo "$body" | grep -Eo '#[0-9]+' | tr -d '#' | sort -u || true)
166+
echo "issues=$issues" >> $GITHUB_OUTPUT
167+
- name: Install gh + jq
168+
if: steps.issues.outputs.issues != ''
169+
run: |
170+
sudo apt-get update && sudo apt-get install -y jq
171+
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
172+
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
173+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
174+
sudo apt-get update && sudo apt-get install gh -y
175+
- name: Set Status=Done
176+
if: steps.issues.outputs.issues != ''
177+
env:
178+
ISSUES: ${{ steps.issues.outputs.issues }}
179+
run: |
180+
gh auth status || gh auth login --with-token <<<"${GITHUB_TOKEN}"
181+
LOGIN=${{ env.GITHUB_USER }}
182+
PROJECT_NUM=${{ env.PROJECT_NUMBER }}
183+
projectQuery='query($login:String!,$number:Int!){ user(login:$login){ projectV2(number:$number){ id fields(first:50){ nodes { __typename ... on ProjectV2SingleSelectField { id name options { id name } } } } items(first:200){ nodes { id content { __typename ... on Issue { number id state } } } } } } }'
184+
resp=$(gh api graphql -f query="$projectQuery" -F login="$LOGIN" -F number=$PROJECT_NUM)
185+
projectId=$(echo "$resp" | jq -r '.data.user.projectV2.id')
186+
statusFieldId=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .id')
187+
optDone=$(echo "$resp" | jq -r '.data.user.projectV2.fields.nodes[] | select(.name=="Status") | .options[] | select(.name=="'"${STATUS_DONE_NAME}"'") | .id')
188+
updateMutation='mutation($project:ID!,$item:ID!,$field:ID!,$option: String!){ updateProjectV2ItemFieldValue(input:{projectId:$project,itemId:$item,fieldId:$field,value:{singleSelectOptionId:$option}}){ projectV2Item { id } } }'
189+
for issue in $ISSUES; do
190+
itemId=$(echo "$resp" | jq -r --arg n "$issue" '.data.user.projectV2.items.nodes[] | select(.content.number==($n|tonumber)) | .id')
191+
if [ -n "$itemId" ]; then
192+
gh api graphql -f query="$updateMutation" -f project=$projectId -f item=$itemId -f field=$statusFieldId -f option=$optDone >/dev/null || true
193+
fi
194+
done
195+
196+
notes:
197+
name: Guidance (no-op)
198+
runs-on: ubuntu-latest
199+
steps:
200+
- name: Display guidance
201+
run: |
202+
echo "Project Sync workflow installed. IMPORTANT:"
203+
echo "1. Update PROJECT_NUMBER in the workflow to match your project's number."
204+
echo "2. Ensure the project has a 'Status' single-select field with options: To do, In Progress, Done."
205+
echo "3. (Optional) Configure built-in project workflows: On item added -> Status=To do; On item closed -> Status=Done."

CODE_OF_CONDUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Unacceptable behaviors include:
1616
- Publishing others' private information
1717

1818
## Reporting
19-
Report issues to the maintainers via [email protected] (replace with real address). Include:
19+
Report issues to the maintainers via [email protected] Include:
2020
- Description
2121
- Links/logs
2222
- Reproduction steps

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Prototype phase: no formal versioning yet. Main branch is mutable.
55

66
## Reporting a Vulnerability
7-
Please email [email protected] (replace with real address) with:
7+
Please email [email protected] with:
88
- Description & impact assessment
99
- Reproduction steps / PoC
1010
- Suggested remediation (if available)

Sources/VMCore/VirtualizationController.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,23 @@ public final class VirtualizationController: NSObject, VirtualMachineControlling
7272
}
7373
// Shared folders (placeholder): In future use VZVirtioFileSystemDeviceConfiguration (virtio-fs)
7474
if !configuration.sharedFolders.isEmpty {
75-
for folder in configuration.sharedFolders {
76-
LoggerFacade.info(.lifecycle, "Shared folder host=\(folder.hostPath.path) guest=\(folder.guestPath) ro=\(folder.readOnly)")
77-
}
7875
#if canImport(Virtualization)
79-
// Current approach: expose only the first shared folder via virtio-fs (tag: "share0")
80-
if let first = configuration.sharedFolders.first {
81-
let share = VZSingleDirectoryShare(directory: first.hostPath)
82-
let fsConfig = VZVirtioFileSystemDeviceConfiguration(tag: "share0")
76+
var fsDevices: [VZVirtioFileSystemDeviceConfiguration] = []
77+
let maxExports = 8 // pragmatic upper bound; adjust if framework allows more
78+
for (idx, folder) in configuration.sharedFolders.enumerated() {
79+
LoggerFacade.info(.lifecycle, "Shared folder host=\(folder.hostPath.path) guest=\(folder.guestPath) ro=\(folder.readOnly)")
80+
if idx >= maxExports { LoggerFacade.error(.lifecycle, "Exceeded max shared folder exports (\(maxExports)); remaining ignored"); break }
81+
let share = VZSingleDirectoryShare(directory: folder.hostPath)
82+
let tag = "share\(idx)" // unique tag per export
83+
let fsConfig = VZVirtioFileSystemDeviceConfiguration(tag: tag)
8384
fsConfig.share = share
84-
cfg.directorySharingDevices = [fsConfig]
85-
LoggerFacade.info(.lifecycle, "Configured virtio-fs tag=share0 (only first folder exported)")
85+
fsDevices.append(fsConfig)
86+
}
87+
if !fsDevices.isEmpty { cfg.directorySharingDevices = fsDevices }
88+
LoggerFacade.info(.lifecycle, "Configured virtio-fs exports count=\(fsDevices.count)")
89+
#else
90+
for folder in configuration.sharedFolders {
91+
LoggerFacade.info(.lifecycle, "(no-virt) Shared folder declared host=\(folder.hostPath.path) guest=\(folder.guestPath)")
8692
}
8793
#endif
8894
}

0 commit comments

Comments
 (0)