Skip to content

Commit de323ee

Browse files
committed
actions
1 parent a74844a commit de323ee

15 files changed

Lines changed: 17760 additions & 121 deletions

‎CODEOWNERS‎

Lines changed: 0 additions & 7 deletions
This file was deleted.

‎__tests__/main.test.ts‎

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,93 @@
11
/**
22
* Unit tests for the action's main functionality, src/main.ts
3-
*
4-
* To mock dependencies in ESM, you can create fixtures that export mock
5-
* functions and objects. For example, the core module is mocked in this test,
6-
* so that the actual '@actions/core' module is not imported.
73
*/
84
import { jest } from '@jest/globals'
95
import * as core from '../__fixtures__/core.js'
10-
import { wait } from '../__fixtures__/wait.js'
116

127
// Mocks should be declared before the module being tested is imported.
138
jest.unstable_mockModule('@actions/core', () => core)
14-
jest.unstable_mockModule('../src/wait.js', () => ({ wait }))
9+
jest.unstable_mockModule('node:fs', () => ({
10+
writeFileSync: jest.fn<(...args: unknown[]) => void>()
11+
}))
12+
13+
// Mock gensvg to avoid heavy physics simulation in tests
14+
const mockGensvg = jest
15+
.fn<(...args: unknown[]) => Promise<string>>()
16+
.mockResolvedValue('<svg>test</svg>' as never)
17+
jest.unstable_mockModule('../src/gensvg.js', () => ({
18+
gensvg: mockGensvg
19+
}))
1520

16-
// The module being tested should be imported dynamically. This ensures that the
17-
// mocks are used in place of any actual dependencies.
1821
const { run } = await import('../src/main.js')
1922

2023
describe('main.ts', () => {
2124
beforeEach(() => {
22-
// Set the action's inputs as return values from core.getInput().
23-
core.getInput.mockImplementation(() => '500')
24-
25-
// Mock the wait function so that it does not actually wait.
26-
wait.mockImplementation(() => Promise.resolve('done!'))
25+
core.getInput.mockImplementation((name: string) => {
26+
if (name === 'username') return 'testuser'
27+
return ''
28+
})
2729
})
2830

2931
afterEach(() => {
3032
jest.resetAllMocks()
3133
})
3234

33-
it('Sets the time output', async () => {
35+
it('generates SVG and sets output', async () => {
3436
await run()
3537

36-
// Verify the time output was set.
37-
expect(core.setOutput).toHaveBeenNthCalledWith(
38-
1,
39-
'time',
40-
// Simple regex to match a time string in the format HH:MM:SS.
41-
expect.stringMatching(/^\d{2}:\d{2}:\d{2}/)
42-
)
38+
expect(core.setOutput).toHaveBeenCalledWith('svg-path', 'gravity.svg')
4339
})
4440

45-
it('Sets a failed status', async () => {
46-
// Clear the getInput mock and return an invalid value.
47-
core.getInput.mockClear().mockReturnValueOnce('this is not a number')
41+
it('uses custom output path', async () => {
42+
core.getInput.mockImplementation((name: string) => {
43+
if (name === 'username') return 'testuser'
44+
if (name === 'output-path') return 'custom.svg'
45+
return ''
46+
})
47+
48+
await run()
49+
50+
expect(core.setOutput).toHaveBeenCalledWith('svg-path', 'custom.svg')
51+
})
52+
53+
it('uses text input when provided', async () => {
54+
core.getInput.mockImplementation((name: string) => {
55+
if (name === 'username') return 'testuser'
56+
if (name === 'text') return 'Custom Text'
57+
return ''
58+
})
59+
60+
await run()
4861

49-
// Clear the wait mock and return a rejected promise.
50-
wait
51-
.mockClear()
52-
.mockRejectedValueOnce(new Error('milliseconds is not a number'))
62+
expect(mockGensvg).toHaveBeenCalledWith(
63+
expect.objectContaining({
64+
username: 'testuser',
65+
text: 'Custom Text'
66+
})
67+
)
68+
})
69+
70+
it('falls back to username when text is empty', async () => {
71+
core.getInput.mockImplementation((name: string) => {
72+
if (name === 'username') return 'testuser'
73+
return ''
74+
})
5375

5476
await run()
5577

56-
// Verify that the action was marked as failed.
57-
expect(core.setFailed).toHaveBeenNthCalledWith(
58-
1,
59-
'milliseconds is not a number'
78+
expect(mockGensvg).toHaveBeenCalledWith(
79+
expect.objectContaining({
80+
username: 'testuser',
81+
text: 'testuser'
82+
})
6083
)
6184
})
85+
86+
it('sets failed status on error', async () => {
87+
mockGensvg.mockRejectedValueOnce(new Error('Simulation failed'))
88+
89+
await run()
90+
91+
expect(core.setFailed).toHaveBeenCalledWith('Simulation failed')
92+
})
6293
})

‎action.yml‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1-
name: The name of your action here
2-
description: Provide a description here
3-
author: Your name or organization here
1+
name: GitHub Gravity
2+
description: Generate an animated SVG of your GitHub contribution graph falling under gravity, with custom text as collision obstacles.
3+
author: flycran
44

5-
# Add your action's branding here. This will appear on the GitHub Marketplace.
65
branding:
7-
icon: heart
8-
color: red
6+
icon: git-pull-request
7+
color: green
98

10-
# Define your inputs here.
119
inputs:
12-
milliseconds:
13-
description: Your input description here
10+
username:
11+
description: GitHub username to fetch contributions for
1412
required: true
15-
default: '1000'
13+
text:
14+
description: Text to display as collision obstacles (defaults to username)
15+
required: false
16+
output-path:
17+
description: Path to write the generated SVG file
18+
required: false
19+
default: gravity.svg
20+
font-size:
21+
description: Font size of the collision text
22+
required: false
23+
default: '80'
24+
sample-rate:
25+
description: Trajectory sample rate (higher = smaller SVG, less smooth animation)
26+
required: false
27+
default: '4'
1628

17-
# Define your outputs here.
1829
outputs:
19-
time:
20-
description: Your output description here
30+
svg-path:
31+
description: Path to the generated SVG file
2132

2233
runs:
2334
using: node24

‎dist/index.js‎

Lines changed: 17536 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/index.js.map‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎jest.config.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default {
3131
'^.+\\.ts$': [
3232
'ts-jest',
3333
{
34-
tsconfig: 'tsconfig.json',
34+
tsconfig: 'tsconfig.jest.json',
3535
useESM: true
3636
}
3737
]

‎package.json‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "typescript-action",
2+
"name": "github-gravity",
33
"description": "GitHub Actions TypeScript template",
44
"version": "0.0.0",
55
"author": "",
@@ -13,7 +13,9 @@
1313
"bugs": {
1414
"url": "https://github.com/actions/typescript-action/issues"
1515
},
16-
"keywords": ["actions"],
16+
"keywords": [
17+
"actions"
18+
],
1719
"exports": {
1820
".": "./dist/index.js"
1921
},
@@ -70,4 +72,4 @@
7072
"optionalDependencies": {
7173
"@rollup/rollup-linux-x64-gnu": "*"
7274
}
73-
}
75+
}

‎rollup.config.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ const config = {
1212
format: 'es',
1313
sourcemap: true
1414
},
15-
plugins: [typescript(), nodeResolve({ preferBuiltins: true }), commonjs()]
15+
plugins: [
16+
typescript({
17+
tsconfig: 'tsconfig.bun.json',
18+
compilerOptions: { composite: false }
19+
}),
20+
nodeResolve({ preferBuiltins: true }),
21+
commonjs()
22+
]
1623
}
1724

1825
export default config

‎src/app.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { startSimulation } from './server/physics'
33
import { GITHUB_ID, TEXT } from './utils'
44
import pageApp from './web/index.html'
55

6-
const res = await startSimulation(GITHUB_ID, TEXT)
7-
86
Bun.serve({
97
port: 3000,
108
development: {
@@ -14,10 +12,20 @@ Bun.serve({
1412
routes: {
1513
'/': pageApp,
1614
'/trajectory': async () => {
17-
return Response.json(res)
15+
return Response.json(
16+
await startSimulation({
17+
username: GITHUB_ID,
18+
text: TEXT,
19+
sampleRate: 8
20+
})
21+
)
1822
},
1923
'/svg': async () => {
20-
const svg = await gensvg({ username: GITHUB_ID, text: TEXT })
24+
const svg = await gensvg({
25+
username: GITHUB_ID,
26+
text: TEXT,
27+
sampleRate: 8
28+
})
2129
return new Response(svg, {
2230
headers: { 'Content-Type': 'image/svg+xml' }
2331
})

‎src/gensvg.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { HEIGHT, SCALE, SIZE, WIDTH } from './utils'
55
export interface GensvgOptions {
66
username: string
77
text: string
8+
fontSize?: number
9+
/** 轨迹采样率,每隔 N 步记录一个轨迹点。默认为 1(不采样)。值越大,SVG 越小 */
10+
sampleRate?: number
811
}
912

1013
/**
@@ -16,9 +19,16 @@ export interface GensvgOptions {
1619
*/
1720
export async function gensvg({
1821
username,
19-
text
22+
text,
23+
fontSize,
24+
sampleRate
2025
}: GensvgOptions): Promise<string> {
21-
const result: SimulationResult = await startSimulation(username, text)
26+
const result: SimulationResult = await startSimulation({
27+
username,
28+
text,
29+
fontSize,
30+
sampleRate
31+
})
2232

2333
const svgWidth = WIDTH * SCALE
2434
const svgHeight = HEIGHT * SCALE

0 commit comments

Comments
 (0)