@zenithbuild/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.eslintignore +15 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/compiler-errors-for-invalid-state-declarations.md +25 -0
  4. package/.github/ISSUE_TEMPLATE/new_ticket.yaml +34 -0
  5. package/.github/pull_request_template.md +15 -0
  6. package/.github/workflows/discord-changelog.yml +141 -0
  7. package/.github/workflows/discord-notify.yml +242 -0
  8. package/.github/workflows/discord-version.yml +195 -0
  9. package/.prettierignore +13 -0
  10. package/.prettierrc +21 -0
  11. package/.zen.d.ts +15 -0
  12. package/LICENSE +21 -0
  13. package/README.md +55 -0
  14. package/app/components/Button.zen +46 -0
  15. package/app/components/Link.zen +11 -0
  16. package/app/favicon.ico +0 -0
  17. package/app/layouts/Main.zen +59 -0
  18. package/app/pages/about.zen +23 -0
  19. package/app/pages/blog/[id].zen +53 -0
  20. package/app/pages/blog/index.zen +32 -0
  21. package/app/pages/dynamic-dx.zen +712 -0
  22. package/app/pages/dynamic-primitives.zen +453 -0
  23. package/app/pages/index.zen +154 -0
  24. package/app/pages/navigation-demo.zen +229 -0
  25. package/app/pages/posts/[...slug].zen +61 -0
  26. package/app/pages/primitives-demo.zen +273 -0
  27. package/assets/logos/0E3B5DDD-605C-4839-BB2E-DFCA8ADC9604.PNG +0 -0
  28. package/assets/logos/760971E5-79A1-44F9-90B9-925DF30F4278.PNG +0 -0
  29. package/assets/logos/8A06ED80-9ED2-4689-BCBD-13B2E95EE8E4.JPG +0 -0
  30. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.PNG +0 -0
  31. package/assets/logos/C691FF58-ED13-4E8D-B6A3-02E835849340.svg +601 -0
  32. package/assets/logos/README.md +54 -0
  33. package/assets/logos/zen.icns +0 -0
  34. package/bun.lock +39 -0
  35. package/compiler/README.md +380 -0
  36. package/compiler/errors/compilerError.ts +24 -0
  37. package/compiler/finalize/finalizeOutput.ts +163 -0
  38. package/compiler/finalize/generateFinalBundle.ts +82 -0
  39. package/compiler/index.ts +44 -0
  40. package/compiler/ir/types.ts +83 -0
  41. package/compiler/legacy/binding.ts +254 -0
  42. package/compiler/legacy/bindings.ts +338 -0
  43. package/compiler/legacy/component-process.ts +1208 -0
  44. package/compiler/legacy/component.ts +301 -0
  45. package/compiler/legacy/event.ts +50 -0
  46. package/compiler/legacy/expression.ts +1149 -0
  47. package/compiler/legacy/mutation.ts +280 -0
  48. package/compiler/legacy/parse.ts +299 -0
  49. package/compiler/legacy/split.ts +608 -0
  50. package/compiler/legacy/types.ts +32 -0
  51. package/compiler/output/types.ts +34 -0
  52. package/compiler/parse/detectMapExpressions.ts +102 -0
  53. package/compiler/parse/parseScript.ts +22 -0
  54. package/compiler/parse/parseTemplate.ts +425 -0
  55. package/compiler/parse/parseZenFile.ts +66 -0
  56. package/compiler/parse/trackLoopContext.ts +82 -0
  57. package/compiler/runtime/dataExposure.ts +291 -0
  58. package/compiler/runtime/generateDOM.ts +144 -0
  59. package/compiler/runtime/generateHydrationBundle.ts +383 -0
  60. package/compiler/runtime/hydration.ts +309 -0
  61. package/compiler/runtime/navigation.ts +432 -0
  62. package/compiler/runtime/thinRuntime.ts +160 -0
  63. package/compiler/runtime/transformIR.ts +256 -0
  64. package/compiler/runtime/wrapExpression.ts +84 -0
  65. package/compiler/runtime/wrapExpressionWithLoop.ts +77 -0
  66. package/compiler/spa-build.ts +1000 -0
  67. package/compiler/test/validate-test.ts +104 -0
  68. package/compiler/transform/generateBindings.ts +47 -0
  69. package/compiler/transform/generateHTML.ts +28 -0
  70. package/compiler/transform/transformNode.ts +126 -0
  71. package/compiler/transform/transformTemplate.ts +38 -0
  72. package/compiler/validate/validateExpressions.ts +168 -0
  73. package/core/index.ts +135 -0
  74. package/core/lifecycle/index.ts +49 -0
  75. package/core/lifecycle/zen-mount.ts +182 -0
  76. package/core/lifecycle/zen-unmount.ts +88 -0
  77. package/core/reactivity/index.ts +54 -0
  78. package/core/reactivity/tracking.ts +167 -0
  79. package/core/reactivity/zen-batch.ts +57 -0
  80. package/core/reactivity/zen-effect.ts +139 -0
  81. package/core/reactivity/zen-memo.ts +146 -0
  82. package/core/reactivity/zen-ref.ts +52 -0
  83. package/core/reactivity/zen-signal.ts +121 -0
  84. package/core/reactivity/zen-state.ts +180 -0
  85. package/core/reactivity/zen-untrack.ts +44 -0
  86. package/docs/COMMENTS.md +111 -0
  87. package/docs/COMMITS.md +36 -0
  88. package/docs/CONTRIBUTING.md +116 -0
  89. package/docs/STYLEGUIDE.md +62 -0
  90. package/package.json +44 -0
  91. package/router/index.ts +76 -0
  92. package/router/manifest.ts +314 -0
  93. package/router/navigation/ZenLink.zen +231 -0
  94. package/router/navigation/index.ts +78 -0
  95. package/router/navigation/zen-link.ts +584 -0
  96. package/router/runtime.ts +458 -0
  97. package/router/types.ts +168 -0
  98. package/runtime/build.ts +17 -0
  99. package/runtime/serve.ts +93 -0
  100. package/scripts/webhook-proxy.ts +213 -0
  101. package/tsconfig.json +28 -0
package/.eslintignore ADDED
@@ -0,0 +1,15 @@
1
+ # ESLint ignore file
2
+ # .zen files are ignored temporarily until proper language support is added in Phase 3+
3
+
4
+ # Ignore .zen files (custom Zenith syntax)
5
+ **/*.zen
6
+
7
+ # Standard ignores
8
+ node_modules/
9
+ dist/
10
+ out/
11
+ *.log
12
+
13
+
14
+
15
+
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,25 @@
1
+ # Compiler errors for invalid state declarations and usage
2
+
3
+ ## Description
4
+ Add compiler enforcement for the following invalid scenarios:
5
+ - Use of undeclared state variables: `{{ unknown }}`.
6
+ - Use of expressions in placeholders: `{{ count + 1 }}`.
7
+ - Invalid state initialization: `state count = count + 1;`.
8
+ - State mutations outside allowed event handlers.
9
+
10
+ ## Acceptance Criteria
11
+
12
+ 1. Compiler throws errors for undeclared state usage in placeholders.
13
+ 2. Compiler disallows expressions within placeholders.
14
+ 3. Errors are thrown for invalid state initialization logic.
15
+ 4. State mutations are only allowed inside event handlers.
16
+ 5. Test cases ensure all invalid scenarios are caught at compile time.
17
+
18
+ ### Example:
19
+ ```html
20
+ <script>
21
+ state count = 5;
22
+ state other = count + 1; // Error
23
+ </script>
24
+ <p>{{ count + 1 }}</p> <!-- Error -->
25
+ <p>{{ unknown }}</p> <!-- Error -->
@@ -0,0 +1,34 @@
1
+ name: "\U0001F4A1 New Issue"
2
+ description: Suggest a new task
3
+
4
+ title: 'nn-your-issue'
5
+
6
+ body:
7
+ - type: markdown
8
+ attributes:
9
+ value: |
10
+ _Thank you for taking the time to propose a new idea_
11
+
12
+ - type: textarea
13
+ id: problem
14
+ attributes:
15
+ label: '<h2>Describe the Problem</h2>'
16
+ description: Please provide a short user story **DESCRIPTION** of the **PROBLEM**
17
+ placeholder: As a user I...
18
+ validations:
19
+ required: true
20
+
21
+ - type: textarea
22
+ id: solution
23
+ attributes:
24
+ label: '<h2>Acceptance Criteria</h2>'
25
+ description: Please provide the REQUIREMENTS that must be fulfilled to consider this issue resolved.
26
+ placeholder: X must show Y when Z...
27
+ validations:
28
+ required: true
29
+
30
+ - type: textarea
31
+ id: references
32
+ attributes:
33
+ label: '<h2>References</h2>'
34
+ description: Please provide any **SCREENSHOTS**, **CODE SNIPPETS** or **LINKS** to the codebase or documentation which will help implement a solution.
@@ -0,0 +1,15 @@
1
+ # ๐Ÿš€ Summary
2
+
3
+ This merge implements/resolves... (include relevant ticket)
4
+
5
+ ## ๐Ÿ“ How can we Reproduce/Test?
6
+
7
+ Please provide instructions so we can reproduce:
8
+
9
+ - [ ] Step 1
10
+ - [ ] Step 2
11
+ - [ ] ...
12
+
13
+ ## ๐Ÿ“ธ Any screenshots or links to points in your code or references elsewhere as needed
14
+
15
+ Please delete if not relevant.
@@ -0,0 +1,141 @@
1
+ name: Discord Changelog Notifications
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ types: [closed]
9
+
10
+ # Security: Limit permissions to only what's needed
11
+ permissions:
12
+ contents: read
13
+ pull-requests: read
14
+
15
+ jobs:
16
+ notify:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - name: Send Changelog to Discord
20
+ env:
21
+ DISCORD_CHANGELOG_WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}
22
+ run: |
23
+ set -e # Exit on error
24
+ set +x # Don't echo commands (prevents secret exposure in logs)
25
+
26
+ # Mask the webhook URL in logs to prevent accidental exposure
27
+ echo "::add-mask::$DISCORD_CHANGELOG_WEBHOOK_URL"
28
+
29
+ # Validate secret is set
30
+ if [ -z "$DISCORD_CHANGELOG_WEBHOOK_URL" ]; then
31
+ echo "โŒ DISCORD_CHANGELOG_WEBHOOK_URL secret is not set"
32
+ exit 1
33
+ fi
34
+
35
+ # Validate webhook URL format for security
36
+ if [[ ! "$DISCORD_CHANGELOG_WEBHOOK_URL" =~ ^https://discord\.com/api/webhooks/[0-9]+/[A-Za-z0-9_-]+$ ]]; then
37
+ echo "โŒ Invalid Discord webhook URL format"
38
+ exit 1
39
+ fi
40
+
41
+ # Determine event type and get data
42
+ if [ "${{ github.event_name }}" = "push" ]; then
43
+ EVENT_TYPE="push"
44
+ BRANCH="${{ github.ref_name }}"
45
+ PUSHER="${{ github.event.pusher.name }}"
46
+ COMPARE_URL="${{ github.event.compare }}"
47
+
48
+ # Get commit messages and count
49
+ COMMITS_JSON='${{ toJSON(github.event.commits) }}'
50
+ COMMITS=$(echo "$COMMITS_JSON" | jq -r '.[] | "- " + (.message | split("\n")[0])' | head -10)
51
+ COMMITS_COUNT=$(echo "$COMMITS_JSON" | jq 'length')
52
+
53
+ COLOR="3447003" # Blue
54
+ EMOJI="๐Ÿ“ฆ"
55
+ TITLE="Code Pushed"
56
+ # Escape backticks for shell variable
57
+ BRANCH_ESC=$(echo "$BRANCH" | sed "s/\\\`/\\\\\\\`/g")
58
+ DESCRIPTION="**$COMMITS_COUNT commit(s)** pushed to branch \`$BRANCH_ESC\` by $PUSHER"
59
+
60
+ elif [ "${{ github.event.pull_request.merged }}" = "true" ]; then
61
+ EVENT_TYPE="merge"
62
+ PR_NUMBER="${{ github.event.pull_request.number }}"
63
+ PR_TITLE="${{ github.event.pull_request.title }}"
64
+ PR_AUTHOR="${{ github.event.pull_request.user.login }}"
65
+ PR_URL="${{ github.event.pull_request.html_url }}"
66
+ BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
67
+ HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
68
+
69
+ COLOR="3066993" # Green
70
+ EMOJI="๐Ÿ”€"
71
+ TITLE="Pull Request Merged"
72
+ # Escape backticks and newlines
73
+ HEAD_BRANCH_ESC=$(echo "$HEAD_BRANCH" | sed "s/\\\`/\\\\\\\`/g")
74
+ BASE_BRANCH_ESC=$(echo "$BASE_BRANCH" | sed "s/\\\`/\\\\\\\`/g")
75
+ PR_TITLE_ESC=$(echo "$PR_TITLE" | sed "s/\\\`/\\\\\\\`/g" | sed 's/$/\\n/' | tr -d '\n' | sed 's/\\n$//')
76
+ DESCRIPTION="**PR #$PR_NUMBER**: $PR_TITLE_ESC\nMerged \`$HEAD_BRANCH_ESC\` โ†’ \`$BASE_BRANCH_ESC\` by $PR_AUTHOR"
77
+ COMPARE_URL="$PR_URL"
78
+ COMMITS=""
79
+ else
80
+ echo "โญ๏ธ PR not merged, skipping..."
81
+ exit 0
82
+ fi
83
+
84
+ # Export variables for Node.js script
85
+ export EMOJI TITLE COLOR
86
+
87
+ # Use Node.js to properly build JSON payload with escaping
88
+ cat > /tmp/discord_changelog_payload.js <<JS_EOF
89
+ const repo = "${{ github.repository }}";
90
+ const emoji = process.env.EMOJI || '๐Ÿ“ฆ';
91
+ const title = process.env.TITLE || 'Code Updated';
92
+ const color = parseInt(process.env.COLOR || '3447003', 10);
93
+ const description = '$DESCRIPTION';
94
+ const compareUrl = '$COMPARE_URL';
95
+ const commits = '$COMMITS';
96
+
97
+ // Build embed
98
+ const embed = {
99
+ title: emoji + ' ' + title,
100
+ description: description,
101
+ url: compareUrl || 'https://github.com/' + repo,
102
+ color: color,
103
+ fields: commits && commits.length > 0 ? [
104
+ {
105
+ name: 'Recent Commits',
106
+ value: commits.length > 1000 ? commits.substring(0, 1000) + '...' : commits,
107
+ inline: false
108
+ }
109
+ ] : [],
110
+ footer: {
111
+ text: repo
112
+ },
113
+ timestamp: new Date().toISOString()
114
+ };
115
+
116
+ console.log(JSON.stringify({ embeds: [embed] }));
117
+ JS_EOF
118
+
119
+ # Run the Node.js script
120
+ PAYLOAD=$(node /tmp/discord_changelog_payload.js)
121
+
122
+ # Send to Discord with error handling
123
+ HTTP_CODE=$(curl -s -o /tmp/discord_response.txt -w "%{http_code}" \
124
+ -X POST "$DISCORD_CHANGELOG_WEBHOOK_URL" \
125
+ -H "Content-Type: application/json" \
126
+ -d "$PAYLOAD")
127
+
128
+ # Check response
129
+ if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
130
+ echo "โœ… Successfully sent changelog notification to Discord"
131
+ else
132
+ echo "โŒ Failed to send notification (HTTP $HTTP_CODE)"
133
+ if [ -f /tmp/discord_response.txt ]; then
134
+ echo "Response: $(cat /tmp/discord_response.txt)"
135
+ fi
136
+ exit 1
137
+ fi
138
+
139
+ # Clean up
140
+ rm -f /tmp/discord_response.txt /tmp/discord_changelog_payload.js
141
+
@@ -0,0 +1,242 @@
1
+ name: Discord Issue Notifications
2
+
3
+ on:
4
+ issues:
5
+ types: [opened, closed, reopened, edited, assigned, unassigned, labeled, unlabeled]
6
+ issue_comment:
7
+ types: [created]
8
+
9
+ # Security: Limit permissions to only what's needed
10
+ permissions:
11
+ issues: write # Needed to add labels to track notified issues
12
+ contents: read
13
+
14
+ jobs:
15
+ notify:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Send to Discord
19
+ env:
20
+ DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
21
+ run: |
22
+ set -e # Exit on error
23
+ set +x # Don't echo commands (prevents secret exposure in logs)
24
+
25
+ # Mask the webhook URL in logs to prevent accidental exposure
26
+ echo "::add-mask::$DISCORD_WEBHOOK_URL"
27
+
28
+ # Validate secret is set
29
+ if [ -z "$DISCORD_WEBHOOK_URL" ]; then
30
+ echo "โŒ DISCORD_WEBHOOK_URL secret is not set"
31
+ exit 1
32
+ fi
33
+
34
+ # Validate webhook URL format for security
35
+ if [[ ! "$DISCORD_WEBHOOK_URL" =~ ^https://discord\.com/api/webhooks/[0-9]+/[A-Za-z0-9_-]+$ ]]; then
36
+ echo "โŒ Invalid Discord webhook URL format"
37
+ exit 1
38
+ fi
39
+
40
+ # Get issue number and check if already notified
41
+ ISSUE_NUMBER="${{ github.event.issue.number }}"
42
+ REPO="${{ github.repository }}"
43
+ TRACKING_LABEL="discord-notified"
44
+ ACTION="${{ github.event.action }}"
45
+
46
+ # Check if issue already has the tracking label (already notified)
47
+ HAS_LABEL=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json labels --jq ".labels[] | select(.name == \"$TRACKING_LABEL\") | .name" 2>/dev/null || echo "")
48
+
49
+ # Prevent duplicates based on event type
50
+ case "$ACTION" in
51
+ opened)
52
+ # For new issues, skip if already notified
53
+ if [ -n "$HAS_LABEL" ]; then
54
+ echo "โญ๏ธ Issue #$ISSUE_NUMBER already notified to Discord, skipping..."
55
+ exit 0
56
+ fi
57
+ ;;
58
+ edited)
59
+ # For edits: if not notified yet, treat as initial notification (for backfilling)
60
+ # If already notified, send update notification
61
+ if [ -z "$HAS_LABEL" ]; then
62
+ echo "โ„น๏ธ Issue #$ISSUE_NUMBER hasn't been initially notified yet, treating edit as initial notification..."
63
+ # Change action to "opened" for initial notification formatting
64
+ ACTION="opened"
65
+ fi
66
+ ;;
67
+ labeled|unlabeled)
68
+ # For label changes: if not notified yet, treat as initial notification (for backfilling)
69
+ # If already notified, send update notification
70
+ if [ -z "$HAS_LABEL" ]; then
71
+ echo "โ„น๏ธ Issue #$ISSUE_NUMBER hasn't been initially notified yet, treating label change as initial notification..."
72
+ # Change action to "opened" for initial notification formatting
73
+ ACTION="opened"
74
+ fi
75
+ ;;
76
+ assigned|unassigned)
77
+ # For assignment changes, only send if issue was already notified (initial notification sent)
78
+ if [ -z "$HAS_LABEL" ]; then
79
+ echo "โญ๏ธ Issue #$ISSUE_NUMBER hasn't been initially notified yet, skipping assignment notification..."
80
+ exit 0
81
+ fi
82
+ ;;
83
+ closed|reopened)
84
+ # Always send for state changes (closed/reopened) - these are important
85
+ # But skip if issue was never initially notified
86
+ if [ -z "$HAS_LABEL" ]; then
87
+ echo "โญ๏ธ Issue #$ISSUE_NUMBER hasn't been initially notified yet, skipping state change notification..."
88
+ exit 0
89
+ fi
90
+ ;;
91
+ esac
92
+
93
+ # Determine color, emoji, and title based on action (ACTION already set above)
94
+ case "$ACTION" in
95
+ opened)
96
+ COLOR="3066993" # Green
97
+ EMOJI="๐Ÿ“"
98
+ TITLE="New Issue Created"
99
+ ;;
100
+ closed)
101
+ COLOR="15158332" # Red
102
+ EMOJI="โœ…"
103
+ TITLE="Issue Closed"
104
+ ;;
105
+ reopened)
106
+ COLOR="15105570" # Orange
107
+ EMOJI="๐Ÿ”„"
108
+ TITLE="Issue Reopened"
109
+ ;;
110
+ edited)
111
+ COLOR="3447003" # Blue
112
+ EMOJI="โœ๏ธ"
113
+ TITLE="Issue Edited"
114
+ ;;
115
+ assigned|unassigned)
116
+ COLOR="10181046" # Purple
117
+ EMOJI="๐Ÿ‘ค"
118
+ TITLE="Issue Assignment Changed"
119
+ ;;
120
+ labeled|unlabeled)
121
+ COLOR="15844367" # Gold
122
+ EMOJI="๐Ÿท๏ธ"
123
+ TITLE="Issue Label Changed"
124
+ ;;
125
+ *)
126
+ COLOR="3447003" # Blue
127
+ EMOJI="๐Ÿ“‹"
128
+ TITLE="Issue Updated"
129
+ ;;
130
+ esac
131
+
132
+ # Export variables for Node.js script
133
+ export EMOJI TITLE COLOR
134
+
135
+ # Use Node.js to properly build JSON payload with escaping
136
+ # Write Node.js script to temp file (no quotes on heredoc to allow GitHub Actions expansion)
137
+ cat > /tmp/discord_payload.js <<JS_EOF
138
+ const issue = ${{ toJSON(github.event.issue) }};
139
+ const repo = "${{ github.repository }}";
140
+ const emoji = process.env.EMOJI || '๐Ÿ“‹';
141
+ const title = process.env.TITLE || 'Issue Updated';
142
+ const color = parseInt(process.env.COLOR || '3447003', 10);
143
+
144
+ // Format labels - use string concatenation to avoid template literal issues
145
+ const labels = issue.labels && issue.labels.length > 0
146
+ ? issue.labels.map(function(l) { return String.fromCharCode(96) + l.name + String.fromCharCode(96); }).join(' ')
147
+ : '*No labels*';
148
+
149
+ // Truncate body
150
+ let body = issue.body || '*No description*';
151
+ if (body.length > 1000) {
152
+ body = body.substring(0, 1000) + '...';
153
+ }
154
+
155
+ // Build embed
156
+ const embed = {
157
+ title: emoji + ' ' + title,
158
+ description: '**' + issue.title + '**',
159
+ url: issue.html_url,
160
+ color: color,
161
+ fields: [
162
+ {
163
+ name: 'Issue #',
164
+ value: '#' + issue.number,
165
+ inline: true
166
+ },
167
+ {
168
+ name: 'State',
169
+ value: issue.state,
170
+ inline: true
171
+ },
172
+ {
173
+ name: 'Labels',
174
+ value: labels,
175
+ inline: false
176
+ },
177
+ {
178
+ name: 'Description',
179
+ value: body,
180
+ inline: false
181
+ }
182
+ ],
183
+ author: {
184
+ name: issue.user.login,
185
+ icon_url: issue.user.avatar_url
186
+ },
187
+ footer: {
188
+ text: repo
189
+ },
190
+ timestamp: new Date().toISOString()
191
+ };
192
+
193
+ console.log(JSON.stringify({ embeds: [embed] }));
194
+ JS_EOF
195
+
196
+ # Run the Node.js script
197
+ PAYLOAD=$(node /tmp/discord_payload.js)
198
+
199
+ # Send to Discord with error handling
200
+ HTTP_CODE=$(curl -s -o /tmp/discord_response.txt -w "%{http_code}" \
201
+ -X POST "$DISCORD_WEBHOOK_URL" \
202
+ -H "Content-Type: application/json" \
203
+ -d "$PAYLOAD")
204
+
205
+ # Check response
206
+ if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
207
+ echo "โœ… Successfully sent notification to Discord"
208
+
209
+ # Mark issue as notified by adding label (for "opened" events or initial "edited" notifications)
210
+ # This prevents duplicate initial notifications
211
+ if [ "$ACTION" = "opened" ] || [ -z "$HAS_LABEL" ]; then
212
+ # Check if label exists, create if not
213
+ LABEL_EXISTS=$(gh label list --repo "$REPO" --json name --jq ".[] | select(.name == \"$TRACKING_LABEL\") | .name" 2>/dev/null || echo "")
214
+
215
+ if [ -z "$LABEL_EXISTS" ]; then
216
+ echo "Creating tracking label: $TRACKING_LABEL"
217
+ gh label create "$TRACKING_LABEL" \
218
+ --repo "$REPO" \
219
+ --color "0E8A16" \
220
+ --description "Issue has been notified to Discord" \
221
+ 2>/dev/null || true
222
+ fi
223
+
224
+ # Add label to issue to mark as notified
225
+ echo "Marking issue #$ISSUE_NUMBER as notified"
226
+ gh issue edit "$ISSUE_NUMBER" \
227
+ --repo "$REPO" \
228
+ --add-label "$TRACKING_LABEL" \
229
+ 2>/dev/null || echo "โš ๏ธ Could not add label (may already exist)"
230
+ fi
231
+ else
232
+ echo "โŒ Failed to send notification (HTTP $HTTP_CODE)"
233
+ # Log error response without exposing webhook URL
234
+ if [ -f /tmp/discord_response.txt ]; then
235
+ echo "Response: $(cat /tmp/discord_response.txt)"
236
+ fi
237
+ exit 1
238
+ fi
239
+
240
+ # Clean up
241
+ rm -f /tmp/discord_response.txt /tmp/discord_payload.js
242
+
@@ -0,0 +1,195 @@
1
+ name: Discord Release Notifications
2
+
3
+ on:
4
+ release:
5
+ types: [published, edited, prereleased, released]
6
+
7
+ # Security: Limit permissions to only what's needed
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ notify:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Send Release Notification to Discord
16
+ env:
17
+ DISCORD_VERSION_WEBHOOK_URL: ${{ secrets.DISCORD_VERSION_WEBHOOK_URL }}
18
+ run: |
19
+ set -e # Exit on error
20
+ set +x # Don't echo commands (prevents secret exposure in logs)
21
+
22
+ # Mask the webhook URL in logs to prevent accidental exposure
23
+ echo "::add-mask::$DISCORD_VERSION_WEBHOOK_URL"
24
+
25
+ # Validate secret is set
26
+ if [ -z "$DISCORD_VERSION_WEBHOOK_URL" ]; then
27
+ echo "โŒ DISCORD_VERSION_WEBHOOK_URL secret is not set"
28
+ exit 1
29
+ fi
30
+
31
+ # Validate webhook URL format for security
32
+ if [[ ! "$DISCORD_VERSION_WEBHOOK_URL" =~ ^https://discord\.com/api/webhooks/[0-9]+/[A-Za-z0-9_-]+$ ]]; then
33
+ echo "โŒ Invalid Discord webhook URL format"
34
+ exit 1
35
+ fi
36
+
37
+ # Get release information
38
+ RELEASE_TAG="${{ github.event.release.tag_name }}"
39
+ RELEASE_NAME="${{ github.event.release.name }}"
40
+ RELEASE_BODY="${{ github.event.release.body }}"
41
+ RELEASE_URL="${{ github.event.release.html_url }}"
42
+ RELEASE_AUTHOR="${{ github.event.release.author.login }}"
43
+ RELEASE_PRERELEASE="${{ github.event.release.prerelease }}"
44
+ RELEASE_ACTION="${{ github.event.action }}"
45
+
46
+ REPO="${{ github.repository }}"
47
+
48
+ # Determine release type and formatting
49
+ if [ "$RELEASE_PRERELEASE" = "true" ]; then
50
+ COLOR="15105570" # Orange
51
+ EMOJI="๐Ÿงช"
52
+ TITLE="Pre-Release Published"
53
+ else
54
+ # Parse version to determine type
55
+ VERSION_TYPE=$(node <<VERSION_SCRIPT
56
+ const tag = '$RELEASE_TAG';
57
+ // Remove 'v' prefix if present
58
+ const version = tag.replace(/^v/i, '');
59
+ const parts = version.split('.').map(function(p) { return parseInt(p) || 0; });
60
+
61
+ if (parts[0] > 0 && parts[1] === 0 && parts[2] === 0) {
62
+ console.log('major');
63
+ } else if (parts[1] > 0 && parts[2] === 0) {
64
+ console.log('minor');
65
+ } else {
66
+ console.log('patch');
67
+ }
68
+ VERSION_SCRIPT
69
+ )
70
+
71
+ case "$VERSION_TYPE" in
72
+ major)
73
+ COLOR="15158332" # Red
74
+ EMOJI="๐Ÿš€"
75
+ TITLE="Major Release"
76
+ ;;
77
+ minor)
78
+ COLOR="3066993" # Green
79
+ EMOJI="โœจ"
80
+ TITLE="Minor Release"
81
+ ;;
82
+ patch)
83
+ COLOR="3447003" # Blue
84
+ EMOJI="๐Ÿ”ง"
85
+ TITLE="Patch Release"
86
+ ;;
87
+ *)
88
+ COLOR="3447003" # Blue
89
+ EMOJI="๐Ÿ“ฆ"
90
+ TITLE="Release Published"
91
+ ;;
92
+ esac
93
+ fi
94
+
95
+ # Handle different release actions
96
+ case "$RELEASE_ACTION" in
97
+ published)
98
+ # Already set above
99
+ ;;
100
+ edited)
101
+ COLOR="15844367" # Gold
102
+ EMOJI="โœ๏ธ"
103
+ TITLE="Release Edited"
104
+ ;;
105
+ prereleased)
106
+ COLOR="15105570" # Orange
107
+ EMOJI="๐Ÿงช"
108
+ TITLE="Pre-Release Published"
109
+ ;;
110
+ released)
111
+ # Use version-based formatting
112
+ ;;
113
+ esac
114
+
115
+ # Export variables for Node.js script
116
+ export EMOJI TITLE COLOR
117
+
118
+ # Use Node.js to properly build JSON payload with escaping
119
+ cat > /tmp/discord_version_payload.js <<JS_EOF
120
+ const repo = "$REPO";
121
+ const emoji = process.env.EMOJI || '๐Ÿ“ฆ';
122
+ const title = process.env.TITLE || 'Release Published';
123
+ const color = parseInt(process.env.COLOR || '3447003', 10);
124
+ const releaseTag = '$RELEASE_TAG';
125
+ const releaseName = '$RELEASE_NAME';
126
+ const releaseBody = '$RELEASE_BODY';
127
+ const releaseUrl = '$RELEASE_URL';
128
+ const releaseAuthor = '$RELEASE_AUTHOR';
129
+ const isPrerelease = '$RELEASE_PRERELEASE' === 'true';
130
+
131
+ // Truncate release body if too long
132
+ let body = releaseBody || '*No release notes*';
133
+ if (body.length > 1000) {
134
+ body = body.substring(0, 1000) + '...';
135
+ }
136
+
137
+ // Build embed
138
+ const embed = {
139
+ title: emoji + ' ' + title,
140
+ description: '**' + (releaseName || releaseTag) + '**' + (isPrerelease ? ' (Pre-release)' : ''),
141
+ url: releaseUrl,
142
+ color: color,
143
+ fields: [
144
+ {
145
+ name: 'Tag',
146
+ value: String.fromCharCode(96) + releaseTag + String.fromCharCode(96),
147
+ inline: true
148
+ },
149
+ {
150
+ name: 'Type',
151
+ value: isPrerelease ? 'Pre-release' : 'Release',
152
+ inline: true
153
+ }
154
+ ],
155
+ footer: {
156
+ text: repo + ' โ€ข ' + releaseAuthor
157
+ },
158
+ timestamp: new Date().toISOString()
159
+ };
160
+
161
+ // Add release notes if available
162
+ if (body && body !== '*No release notes*') {
163
+ embed.fields.push({
164
+ name: 'Release Notes',
165
+ value: body,
166
+ inline: false
167
+ });
168
+ }
169
+
170
+ console.log(JSON.stringify({ embeds: [embed] }));
171
+ JS_EOF
172
+
173
+ # Run the Node.js script
174
+ PAYLOAD=$(node /tmp/discord_version_payload.js)
175
+
176
+ # Send to Discord with error handling
177
+ HTTP_CODE=$(curl -s -o /tmp/discord_response.txt -w "%{http_code}" \
178
+ -X POST "$DISCORD_VERSION_WEBHOOK_URL" \
179
+ -H "Content-Type: application/json" \
180
+ -d "$PAYLOAD")
181
+
182
+ # Check response
183
+ if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
184
+ echo "โœ… Successfully sent version update notification to Discord"
185
+ else
186
+ echo "โŒ Failed to send notification (HTTP $HTTP_CODE)"
187
+ if [ -f /tmp/discord_response.txt ]; then
188
+ echo "Response: $(cat /tmp/discord_response.txt)"
189
+ fi
190
+ exit 1
191
+ fi
192
+
193
+ # Clean up
194
+ rm -f /tmp/discord_response.txt /tmp/discord_version_payload.js
195
+
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ *.lock
5
+ bun.lock
6
+ coverage
7
+ .venv
8
+ __pycache__
9
+
10
+ # .zen files are ignored temporarily until proper language support is added in Phase 3+
11
+ # Ignore .zen files (custom Zenith syntax)
12
+ **/*.zen
13
+