Skip to content

Commit 759f9d5

Browse files
committed
feat: pleasant emojis, fix copilot commentary, add tests
Signed-off-by: Sam Gammon <sam@elide.ventures>
1 parent 949c560 commit 759f9d5

18 files changed

Lines changed: 2186 additions & 163 deletions

.github/workflows/ci.yml

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,55 @@ jobs:
6262
fail-fast: false
6363
matrix:
6464
include:
65-
- name: linux-amd64
65+
# --- Linux AMD64 ---
66+
- name: linux-amd64 (archive)
6667
runner: ubuntu-latest
67-
- name: linux-arm64
68+
installer: archive
69+
- name: linux-amd64 (shell)
70+
runner: ubuntu-latest
71+
installer: shell
72+
- name: linux-amd64 (apt)
73+
runner: ubuntu-latest
74+
installer: apt
75+
# - name: linux-amd64 (rpm)
76+
# runner: ubuntu-latest
77+
# installer: rpm
78+
79+
# --- Linux ARM64 ---
80+
- name: linux-arm64 (archive)
6881
runner: ubuntu-24.04-arm
69-
- name: macos-arm64
82+
installer: archive
83+
- name: linux-arm64 (shell)
84+
runner: ubuntu-24.04-arm
85+
installer: shell
86+
87+
# --- macOS ARM64 ---
88+
- name: macos-arm64 (archive)
89+
runner: macos-latest
90+
installer: archive
91+
- name: macos-arm64 (shell)
7092
runner: macos-latest
71-
- name: macos-amd64
93+
installer: shell
94+
- name: macos-arm64 (pkg)
95+
runner: macos-latest
96+
installer: pkg
97+
98+
# --- macOS AMD64 ---
99+
- name: macos-amd64 (archive)
72100
runner: macos-15-intel
73-
- name: windows-amd64
101+
installer: archive
102+
103+
# --- Windows AMD64 ---
104+
- name: windows-amd64 (archive)
74105
runner: windows-latest
106+
installer: archive
107+
- name: windows-amd64 (shell)
108+
runner: windows-latest
109+
installer: shell
110+
# MSI: suppressed until installer is verified
111+
# - name: windows-amd64 (msi)
112+
# runner: windows-latest
113+
# installer: msi
75114

76115
steps:
77116
- name: Harden Runner
@@ -86,11 +125,14 @@ jobs:
86125
- name: "Test: Local Action"
87126
id: test-action
88127
uses: ./
89-
with: {}
128+
with:
129+
installer: ${{ matrix.installer }}
90130

91131
- name: "Test: Verify Installation"
92132
shell: bash
93133
run: |
94134
elide --version
95135
echo "Path: ${{ steps.test-action.outputs.path }}"
96136
echo "Version: ${{ steps.test-action.outputs.version }}"
137+
echo "Cached: ${{ steps.test-action.outputs.cached }}"
138+
echo "Installer: ${{ steps.test-action.outputs.installer }}"

.github/workflows/release.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ jobs:
4646
- name: "Build: Bundle"
4747
run: bun run build
4848

49+
- name: "Sentry: Upload Source Maps"
50+
if: env.SENTRY_AUTH_TOKEN != ''
51+
env:
52+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
53+
SENTRY_ORG: elide-dev
54+
SENTRY_PROJECT: setup-elide
55+
run: |
56+
npx @sentry/cli releases new "setup-elide@${GITHUB_REF_NAME#v}"
57+
npx @sentry/cli releases files "setup-elide@${GITHUB_REF_NAME#v}" upload-sourcemaps dist/ --ext js --ext map
58+
npx @sentry/cli releases finalize "setup-elide@${GITHUB_REF_NAME#v}"
59+
npx @sentry/cli releases deploys "setup-elide@${GITHUB_REF_NAME#v}" new -e production
60+
4961
- name: "Build: Package Artifact"
5062
run: |
5163
TAG="${GITHUB_REF_NAME}"
@@ -54,6 +66,8 @@ jobs:
5466
action.yml \
5567
dist/index.js \
5668
dist/index.js.map \
69+
dist/post.js \
70+
dist/post.js.map \
5771
package.json \
5872
.github/LICENSE
5973
sha256sum "artifacts/setup-elide-${TAG}.tar.gz" > "artifacts/setup-elide-${TAG}.tar.gz.sha256"

__tests__/install-rpm.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ describe('install-rpm', () => {
128128

129129
expect(execMock).toHaveBeenCalledWith('sudo', [
130130
'rpm',
131-
'-i',
131+
'-U',
132+
'--replacepkgs',
132133
'/tmp/elide.rpm'
133134
])
134135
expect(execMock).not.toHaveBeenCalledWith(

__tests__/main.test.ts

Lines changed: 108 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,22 @@ const debugMock = jest.fn()
1313
const infoMock = jest.fn()
1414
const warningMock = jest.fn()
1515
const errorMock = jest.fn()
16+
const noticeMock = jest.fn()
1617
const addPathMock = jest.fn()
18+
const groupMock = jest.fn(async (_name: string, fn: () => Promise<any>) => fn())
19+
const summaryMock = {
20+
addHeading: jest.fn().mockReturnThis(),
21+
addTable: jest.fn().mockReturnThis(),
22+
addCodeBlock: jest.fn().mockReturnThis(),
23+
addLink: jest.fn().mockReturnThis(),
24+
write: jest.fn().mockResolvedValue(undefined)
25+
}
1726
const downloadToolMock = jest.fn().mockResolvedValue('/tmp/install.sh')
1827
const elideInfoMock = jest.fn().mockResolvedValue(undefined)
1928
const obtainVersionMock = jest.fn().mockResolvedValue('1.0.0')
29+
const initTelemetryMock = jest.fn()
30+
const reportErrorMock = jest.fn()
31+
const flushTelemetryMock = jest.fn().mockResolvedValue(undefined)
2032

2133
// Mock modules before any project imports
2234
mock.module('@actions/exec', () => ({
@@ -35,11 +47,14 @@ mock.module('@actions/core', () => ({
3547
debug: debugMock,
3648
error: errorMock,
3749
warning: warningMock,
50+
notice: noticeMock,
3851
getInput: getInputMock,
3952
getBooleanInput: jest.fn().mockReturnValue(true),
4053
setFailed: setFailedMock,
4154
setOutput: setOutputMock,
42-
addPath: addPathMock
55+
addPath: addPathMock,
56+
group: groupMock,
57+
summary: summaryMock
4358
}))
4459
mock.module('@actions/tool-cache', () => ({
4560
downloadTool: downloadToolMock,
@@ -54,6 +69,11 @@ mock.module('../src/command', () => ({
5469
ElideCommand: { RUN: 'run', INFO: 'info' },
5570
ElideArgument: { VERSION: '--version' }
5671
}))
72+
mock.module('../src/telemetry', () => ({
73+
initTelemetry: initTelemetryMock,
74+
reportError: reportErrorMock,
75+
flushTelemetry: flushTelemetryMock
76+
}))
5777

5878
const main = await import('../src/main')
5979
const { default: buildOptions, OptionName } = await import('../src/options')
@@ -78,7 +98,6 @@ const setupMocks = () => {
7898

7999
describe('action', () => {
80100
beforeEach(() => {
81-
// Clear all mock state
82101
execMock.mockClear()
83102
getExecOutputMock.mockClear()
84103
whichMock.mockClear()
@@ -89,14 +108,25 @@ describe('action', () => {
89108
infoMock.mockClear()
90109
warningMock.mockClear()
91110
errorMock.mockClear()
111+
noticeMock.mockClear()
92112
addPathMock.mockClear()
113+
groupMock.mockClear()
93114
downloadToolMock.mockClear()
94115
elideInfoMock.mockClear()
95116
obtainVersionMock.mockClear()
96-
// Default: getInput returns empty
97-
getInputMock.mockReturnValue('')
117+
initTelemetryMock.mockClear()
118+
reportErrorMock.mockClear()
119+
flushTelemetryMock.mockClear()
120+
summaryMock.addHeading.mockClear()
121+
summaryMock.addTable.mockClear()
122+
summaryMock.write.mockClear()
123+
summaryMock.addCodeBlock.mockClear()
124+
summaryMock.addLink.mockClear()
98125

99-
// Default: install script path succeeds
126+
getInputMock.mockReturnValue('')
127+
groupMock.mockImplementation(
128+
async (_name: string, fn: () => Promise<any>) => fn()
129+
)
100130
downloadToolMock.mockResolvedValue('/tmp/install.sh')
101131
execMock.mockResolvedValue(0)
102132
whichMock.mockResolvedValue('/mock/bin/elide')
@@ -107,6 +137,7 @@ describe('action', () => {
107137
})
108138
elideInfoMock.mockResolvedValue(undefined)
109139
obtainVersionMock.mockResolvedValue('1.0.0')
140+
flushTelemetryMock.mockResolvedValue(undefined)
110141
})
111142

112143
it('reads option inputs', async () => {
@@ -135,15 +166,73 @@ describe('action', () => {
135166
)
136167
})
137168

138-
it('should fail for unhandled exceptions', async () => {
169+
it('sets cached and installer outputs', async () => {
170+
setupMocks()
171+
await main.run({ force: true, installer: 'shell' })
172+
expect(setOutputMock).toHaveBeenCalledWith(ActionOutputName.CACHED, 'false')
173+
expect(setOutputMock).toHaveBeenCalledWith(
174+
ActionOutputName.INSTALLER,
175+
'shell'
176+
)
177+
})
178+
179+
it('should initialize telemetry', async () => {
180+
setupMocks()
181+
await main.run()
182+
expect(initTelemetryMock).toHaveBeenCalled()
183+
})
184+
185+
it('should flush telemetry in finally block', async () => {
186+
setupMocks()
187+
await main.run()
188+
expect(flushTelemetryMock).toHaveBeenCalled()
189+
})
190+
191+
it('should report errors to telemetry on failure', async () => {
139192
setupMocks()
140-
infoMock.mockImplementationOnce(() => {
141-
throw new Error('oh noes')
193+
infoMock.mockImplementation((msg: string) => {
194+
if (msg.includes('Options:')) throw new Error('oh noes')
142195
})
143196
await main.run()
197+
expect(reportErrorMock).toHaveBeenCalled()
144198
expect(setFailedMock).toHaveBeenCalled()
145199
})
146200

201+
it('should use grouped output', async () => {
202+
setupMocks()
203+
await main.run({ force: true, installer: 'shell' })
204+
expect(groupMock).toHaveBeenCalledWith(
205+
'⚙️ Resolving options',
206+
expect.any(Function)
207+
)
208+
expect(groupMock).toHaveBeenCalledWith(
209+
'📦 Installing Elide via shell',
210+
expect.any(Function)
211+
)
212+
expect(groupMock).toHaveBeenCalledWith(
213+
'✅ Verifying installation',
214+
expect.any(Function)
215+
)
216+
})
217+
218+
it('should write job summary on success', async () => {
219+
setupMocks()
220+
await main.run({ force: true, installer: 'shell' })
221+
expect(summaryMock.addHeading).toHaveBeenCalledWith('Elide Installed', 2)
222+
expect(summaryMock.write).toHaveBeenCalled()
223+
})
224+
225+
it('should write error summary on failure', async () => {
226+
setupMocks()
227+
infoMock.mockImplementation((msg: string) => {
228+
if (msg.includes('Options:')) throw new Error('install boom')
229+
})
230+
await main.run()
231+
expect(summaryMock.addHeading).toHaveBeenCalledWith('Setup Elide Failed', 2)
232+
expect(summaryMock.addCodeBlock).toHaveBeenCalled()
233+
expect(summaryMock.write).toHaveBeenCalled()
234+
})
235+
147236
it('should properly detect existing elide binary', async () => {
148237
whichMock.mockResolvedValueOnce('/some/path/to/an/elide/bin')
149238
const existing = await main.resolveExistingBinary()
@@ -161,7 +250,7 @@ describe('action', () => {
161250

162251
it('should use archive installer by default', async () => {
163252
setupMocks()
164-
await main.run({ force: true })
253+
await main.run({ force: true, installer: 'shell' })
165254
expect(setFailedMock).not.toHaveBeenCalled()
166255
expect(setOutputMock).toHaveBeenCalledWith(
167256
ActionOutputName.PATH,
@@ -190,17 +279,6 @@ describe('action', () => {
190279
expect(infoMock).toHaveBeenCalledWith(expect.stringContaining('apt'))
191280
})
192281

193-
it('should use archive download for windows with archive installer', async () => {
194-
setupMocks()
195-
await main.run({
196-
force: true,
197-
os: 'windows',
198-
arch: 'amd64',
199-
installer: 'archive'
200-
})
201-
expect(setFailedMock).not.toHaveBeenCalled()
202-
})
203-
204282
it('should use msi installer on windows', async () => {
205283
setupMocks()
206284
await main.run({
@@ -245,24 +323,12 @@ describe('action', () => {
245323
force: true,
246324
os: 'linux',
247325
arch: 'amd64',
326+
version: '1.0.0',
248327
installer: 'msi'
249328
})
250329
expect(warningMock).toHaveBeenCalledWith(
251-
expect.stringContaining("Installer 'msi' is not supported on linux")
252-
)
253-
expect(setFailedMock).not.toHaveBeenCalled()
254-
})
255-
256-
it('should warn and fall back for pkg on linux', async () => {
257-
setupMocks()
258-
await main.run({
259-
force: true,
260-
os: 'linux',
261-
arch: 'amd64',
262-
installer: 'pkg'
263-
})
264-
expect(warningMock).toHaveBeenCalledWith(
265-
expect.stringContaining("Installer 'pkg' is not supported on linux")
330+
expect.stringContaining("Installer 'msi' is not supported on linux"),
331+
expect.objectContaining({ title: 'Installer Fallback' })
266332
)
267333
expect(setFailedMock).not.toHaveBeenCalled()
268334
})
@@ -277,16 +343,12 @@ describe('action', () => {
277343
ActionOutputName.PATH,
278344
expect.anything()
279345
)
280-
expect(setOutputMock).toHaveBeenCalledWith(
281-
ActionOutputName.VERSION,
282-
expect.anything()
283-
)
284346
})
285347

286348
it('should gracefully handle post-install info failure', async () => {
287349
setupMocks()
288350
elideInfoMock.mockRejectedValueOnce(new Error('info boom'))
289-
await main.run({ force: true })
351+
await main.run({ force: true, installer: 'shell' })
290352
expect(setFailedMock).not.toHaveBeenCalled()
291353
expect(debugMock).toHaveBeenCalledWith(
292354
expect.stringContaining('Post-install info failed; proceeding anyway')
@@ -306,19 +368,19 @@ describe('action', () => {
306368
ActionOutputName.VERSION,
307369
'1.0.0'
308370
)
309-
expect(infoMock).toHaveBeenCalledWith(
310-
expect.stringContaining('was preserved')
371+
expect(noticeMock).toHaveBeenCalledWith(
372+
expect.stringContaining('preserved'),
373+
expect.objectContaining({ title: 'Already Installed' })
311374
)
312375
})
313376

314377
it('should warn on version mismatch', async () => {
315378
setupMocks()
316-
// First call (inside installViaShell) returns '1.0.0' as the installed version,
317-
// second call (main.run verification) returns a different version.
318379
obtainVersionMock.mockResolvedValueOnce('1.0.0').mockResolvedValue('9.9.9')
319380
await main.run({ force: true, installer: 'shell' })
320381
expect(warningMock).toHaveBeenCalledWith(
321-
expect.stringContaining('Elide version mismatch')
382+
expect.stringContaining('Elide version mismatch'),
383+
expect.objectContaining({ title: 'Version Mismatch' })
322384
)
323385
})
324386

@@ -338,7 +400,7 @@ describe('action', () => {
338400

339401
it('should not export to path when export_path is false', async () => {
340402
setupMocks()
341-
await main.run({ force: true, export_path: false })
403+
await main.run({ force: true, export_path: false, installer: 'shell' })
342404
expect(addPathMock).not.toHaveBeenCalled()
343405
expect(setFailedMock).not.toHaveBeenCalled()
344406
})

0 commit comments

Comments
 (0)