mirror of
https://github.com/seepine/hash-files.git
synced 2025-07-12 18:33:13 +08:00
chore(release): v1.0.0
This commit is contained in:
parent
848c6fc5e6
commit
6da8771096
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
|
||||||
|
# 表示是最顶层的 EditorConfig 配置文件
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*] # 表示所有文件适用
|
||||||
|
charset = utf-8 # 设置文件字符集为 utf-8
|
||||||
|
indent_style = space # 缩进风格(tab | space)
|
||||||
|
indent_size = 2 # 缩进大小
|
||||||
|
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||||
|
trim_trailing_whitespace = true # 去除行首的任意空白字符
|
||||||
|
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||||
|
|
||||||
|
[*.md] # 表示仅 md 文件适用以下规则
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
@ -2,3 +2,4 @@ dist/
|
|||||||
lib/
|
lib/
|
||||||
node_modules/
|
node_modules/
|
||||||
jest.config.js
|
jest.config.js
|
||||||
|
__tests__/
|
||||||
|
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@ -15,10 +15,17 @@ jobs:
|
|||||||
npm install
|
npm install
|
||||||
- run: |
|
- run: |
|
||||||
npm run all
|
npm run all
|
||||||
|
|
||||||
test: # make sure the action works on a clean machine without building
|
test: # make sure the action works on a clean machine without building
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: ./
|
- uses: ./
|
||||||
|
id: get-hash
|
||||||
with:
|
with:
|
||||||
milliseconds: 1000
|
patterns: |
|
||||||
|
**/package-lock.json
|
||||||
|
|
||||||
|
- name: Echo hash
|
||||||
|
run: echo ${{ steps.get-hash.outputs.hash }}
|
||||||
|
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
// 保存时执行eslint
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
// 从不展示扩展缩写
|
||||||
|
"emmet.showExpandedAbbreviation": "never",
|
||||||
|
// 关闭代码区域小地图
|
||||||
|
"editor.minimap.enabled": false,
|
||||||
|
// 代码过长换行
|
||||||
|
"editor.wordWrap": "on"
|
||||||
|
}
|
142
README.md
142
README.md
@ -1,105 +1,57 @@
|
|||||||
<p align="center">
|
# hash-files
|
||||||
<a href="https://github.com/actions/typescript-action/actions"><img alt="typescript-action status" src="https://github.com/actions/typescript-action/workflows/build-test/badge.svg"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# Create a JavaScript Action using TypeScript
|
This action is to compute the SHA256 hash of specified files.
|
||||||
|
|
||||||
Use this template to bootstrap the creation of a TypeScript action.:rocket:
|
The hash function is based on [nektos/act](https://github.com/nektos/act/blob/ac5dd8feb876d37ae483376a137c57383577dace/pkg/exprparser/functions.go#L183). Thanks!
|
||||||
|
|
||||||
This template includes compilation support, tests, a validation workflow, publishing, and versioning guidance.
|
**NOTE:** This action is written in Go. Please setup the Go environment before running this action or use a runner with Go environment installed.
|
||||||
|
|
||||||
If you are new, there's also a simpler introduction. See the [Hello World JavaScript Action](https://github.com/actions/hello-world-javascript-action)
|
## Usage
|
||||||
|
|
||||||
## Create an action from this template
|
``` yml
|
||||||
|
- uses: actions/go-hashfiles@v0.0.1
|
||||||
|
with:
|
||||||
|
# The working dir for the action.
|
||||||
|
# Default: ${{ github.workspace }}
|
||||||
|
workdir: ''
|
||||||
|
|
||||||
Click the `Use this Template` and provide the new repo details for your action
|
# The patterns used to match files.
|
||||||
|
patterns: '**/package-lock.json'
|
||||||
|
|
||||||
## Code in Main
|
# Multiple patterns should be seperated by `\n`
|
||||||
|
patterns: |
|
||||||
> First, you'll need to have a reasonably modern version of `node` handy. This won't work with versions older than 9, for instance.
|
**/package-lock.json
|
||||||
|
**/yarn.lock
|
||||||
Install the dependencies
|
|
||||||
```bash
|
|
||||||
$ npm install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the typescript and package it for distribution
|
## Input
|
||||||
```bash
|
|
||||||
$ npm run build && npm run package
|
|Output Item|Description|Required|Default|
|
||||||
|
|---|---|---|---|
|
||||||
|
|workdir|The working dir for the action|false|${{ github.workspace }}|
|
||||||
|
|patterns|The patterns used to match files|true||
|
||||||
|
|gitignore|Respect ignore patterns in .gitignore files that apply to the globbed files.|false|true
|
||||||
|
|ignoreFiles|Glob patterns to look for ignore files, which are then used to ignore globbed files.|false|
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
|Output Item|Description|
|
||||||
|
|---|---|
|
||||||
|
|hash|The computed hash result|
|
||||||
|
|matched-files|The files matched by the patterns|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
``` yml
|
||||||
|
# Setup the Node environment. This step can be skipped if Node has been installed.
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
|
||||||
|
- uses: actions/go-hashfiles@v1
|
||||||
|
id: get-hash
|
||||||
|
with:
|
||||||
|
patterns: |
|
||||||
|
**/package-lock.json
|
||||||
|
**/yarn.lock
|
||||||
|
|
||||||
|
- name: Echo hash
|
||||||
|
run: echo ${{ steps.get-hash.outputs.hash }}
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the tests :heavy_check_mark:
|
|
||||||
```bash
|
|
||||||
$ npm test
|
|
||||||
|
|
||||||
PASS ./index.test.js
|
|
||||||
✓ throws invalid number (3ms)
|
|
||||||
✓ wait 500 ms (504ms)
|
|
||||||
✓ test runs (95ms)
|
|
||||||
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Change action.yml
|
|
||||||
|
|
||||||
The action.yml defines the inputs and output for your action.
|
|
||||||
|
|
||||||
Update the action.yml with your name, description, inputs and outputs for your action.
|
|
||||||
|
|
||||||
See the [documentation](https://help.github.com/en/articles/metadata-syntax-for-github-actions)
|
|
||||||
|
|
||||||
## Change the Code
|
|
||||||
|
|
||||||
Most toolkit and CI/CD operations involve async operations so the action is run in an async function.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
...
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
try {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
core.setFailed(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run()
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [toolkit documentation](https://github.com/actions/toolkit/blob/master/README.md#packages) for the various packages.
|
|
||||||
|
|
||||||
## Publish to a distribution branch
|
|
||||||
|
|
||||||
Actions are run from GitHub repos so we will checkin the packed dist folder.
|
|
||||||
|
|
||||||
Then run [ncc](https://github.com/zeit/ncc) and push the results:
|
|
||||||
```bash
|
|
||||||
$ npm run package
|
|
||||||
$ git add dist
|
|
||||||
$ git commit -a -m "prod dependencies"
|
|
||||||
$ git push origin releases/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: We recommend using the `--license` option for ncc, which will create a license file for all of the production node modules used in your project.
|
|
||||||
|
|
||||||
Your action is now published! :rocket:
|
|
||||||
|
|
||||||
See the [versioning documentation](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md)
|
|
||||||
|
|
||||||
## Validate
|
|
||||||
|
|
||||||
You can now validate the action by referencing `./` in a workflow in your repo (see [test.yml](.github/workflows/test.yml))
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
milliseconds: 1000
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [actions tab](https://github.com/actions/typescript-action/actions) for runs of this action! :rocket:
|
|
||||||
|
|
||||||
## Usage:
|
|
||||||
|
|
||||||
After testing you can [create a v1 tag](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) to reference the stable and latest V1 action
|
|
||||||
|
@ -1,25 +1,34 @@
|
|||||||
import {wait} from '../src/wait'
|
import {getFiles, readFile} from '../src/files'
|
||||||
|
import {hashHex} from '../src/utils'
|
||||||
import * as process from 'process'
|
import * as process from 'process'
|
||||||
import * as cp from 'child_process'
|
import * as cp from 'child_process'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {expect, test} from '@jest/globals'
|
import {test} from '@jest/globals'
|
||||||
|
|
||||||
test('throws invalid number', async () => {
|
test('test sha256', async () => {
|
||||||
const input = parseInt('foo', 10)
|
console.log(hashHex('this is content'))
|
||||||
await expect(wait(input)).rejects.toThrow('milliseconds not a number')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('wait 500 ms', async () => {
|
test('test sha512', async () => {
|
||||||
const start = new Date()
|
console.log(hashHex('this is content', 'sha512'))
|
||||||
await wait(500)
|
})
|
||||||
const end = new Date()
|
|
||||||
var delta = Math.abs(end.getTime() - start.getTime())
|
test('test readFile', async () => {
|
||||||
expect(delta).toBeGreaterThan(450)
|
const content = await readFile('./.prettierignore')
|
||||||
|
console.log(content)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test getFiles', async () => {
|
||||||
|
const paths = await getFiles('./', ['**/*.ts', '**/package-lock.json'], {
|
||||||
|
gitignore: true
|
||||||
|
})
|
||||||
|
console.log(paths)
|
||||||
})
|
})
|
||||||
|
|
||||||
// shows how the runner will run a javascript action with env / stdout protocol
|
|
||||||
test('test runs', () => {
|
test('test runs', () => {
|
||||||
process.env['INPUT_MILLISECONDS'] = '500'
|
process.env['INPUT_WORKDIR'] = './'
|
||||||
|
process.env['INPUT_PATTERNS'] = '**/*.ts\n**/package-lock.json'
|
||||||
|
process.env['INPUT_GITIGNORE'] = 'true'
|
||||||
const np = process.execPath
|
const np = process.execPath
|
||||||
const ip = path.join(__dirname, '..', 'lib', 'main.js')
|
const ip = path.join(__dirname, '..', 'lib', 'main.js')
|
||||||
const options: cp.ExecFileSyncOptions = {
|
const options: cp.ExecFileSyncOptions = {
|
||||||
|
36
action.yml
36
action.yml
@ -1,11 +1,33 @@
|
|||||||
name: 'Your name here'
|
name: 'hash-files'
|
||||||
description: 'Provide a description here'
|
description: 'Compute the SHA256 hash of specified files'
|
||||||
author: 'Your name or organization here'
|
author: 'seepine'
|
||||||
inputs:
|
inputs:
|
||||||
milliseconds: # change this
|
workdir:
|
||||||
|
description: >
|
||||||
|
The working directory for the action.
|
||||||
|
default: ${{ github.workspace }}
|
||||||
|
required: false
|
||||||
|
patterns:
|
||||||
|
description: >
|
||||||
|
The patterns used to match files. You can input multiple patterns seperated by `\n`.
|
||||||
|
We recommand using `|` to concat the patterns in `.yml` files.
|
||||||
required: true
|
required: true
|
||||||
description: 'input description here'
|
gitignore:
|
||||||
default: 'default value if applicable'
|
description: >
|
||||||
|
Respect ignore patterns in .gitignore files that apply to the globbed files.
|
||||||
|
default: true
|
||||||
|
required: false
|
||||||
|
ignoreFiles:
|
||||||
|
descruption: >
|
||||||
|
Glob patterns to look for ignore files, which are then used to ignore globbed files.
|
||||||
|
This is a more generic form of the gitignore option, allowing you to find ignore files with a compatible syntax.
|
||||||
|
For instance, this works with Babel's .babelignore, Prettier's .prettierignore, or ESLint's .eslintignore files.
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
hash:
|
||||||
|
description: 'The computed hash result'
|
||||||
|
matched-files:
|
||||||
|
description: 'The files matched by the patterns'
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
260
dist/index.js
generated
vendored
260
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
3
dist/licenses.txt
generated
vendored
3
dist/licenses.txt
generated
vendored
@ -35,6 +35,9 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|||||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
@cjs-exporter/globby
|
||||||
|
MIT
|
||||||
|
|
||||||
tunnel
|
tunnel
|
||||||
MIT
|
MIT
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
5010
package-lock.json
generated
5010
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "typescript-action",
|
"name": "hash-files",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "TypeScript template action",
|
"description": "Actions of Compute the SHA256 hash of specified files",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"format": "prettier --write '**/*.ts'",
|
"format": "prettier --write **/*.ts",
|
||||||
"format-check": "prettier --check '**/*.ts'",
|
"format-check": "prettier --check **/*.ts",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
"package": "ncc build --source-map --license licenses.txt",
|
"package": "ncc build --source-map --license licenses.txt",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
@ -15,17 +15,19 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/actions/typescript-action.git"
|
"url": "git+https://github.com/seepine/hash-files.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"actions",
|
"actions",
|
||||||
"node",
|
"node",
|
||||||
"setup"
|
"hash-files"
|
||||||
],
|
],
|
||||||
"author": "",
|
"author": "seepine",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0"
|
"@actions/core": "^1.10.0",
|
||||||
|
"@cjs-exporter/globby": "^13.1.3",
|
||||||
|
"crypto": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.16.3",
|
"@types/node": "^18.16.3",
|
||||||
|
12
src/constants.ts
Normal file
12
src/constants.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable no-shadow */
|
||||||
|
export enum Inputs {
|
||||||
|
Workdir = 'workdir', // Input for cache, restore, save action
|
||||||
|
Patterns = 'patterns', // Input for cache, restore, save action
|
||||||
|
Gitignore = 'gitignore', // Input for cache, restore action
|
||||||
|
IgnoreFiles = 'ignoreFiles' // Input for cache, save action
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Outputs {
|
||||||
|
Hash = 'hash', // Output from cache, restore action
|
||||||
|
MatchedFiles = 'matched-files' // Output from restore action
|
||||||
|
}
|
22
src/files.ts
Normal file
22
src/files.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {Options, globby} from '@cjs-exporter/globby'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
export async function getFiles(
|
||||||
|
workdir: string,
|
||||||
|
patterns: string[],
|
||||||
|
options?: Options
|
||||||
|
): Promise<string[]> {
|
||||||
|
return new Promise(async RES => {
|
||||||
|
const paths = await globby(
|
||||||
|
patterns.map(item => {
|
||||||
|
return workdir + item
|
||||||
|
}),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
RES(paths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readFile(path: string): Promise<string> {
|
||||||
|
return fs.promises.readFile(path, 'utf-8')
|
||||||
|
}
|
37
src/main.ts
37
src/main.ts
@ -1,16 +1,37 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {wait} from './wait'
|
import {Inputs, Outputs} from './constants'
|
||||||
|
import * as utils from './utils'
|
||||||
|
import {getFiles, readFile} from './files'
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const ms: string = core.getInput('milliseconds')
|
const workdir: string = utils.getInput(Inputs.Workdir, {required: true})
|
||||||
core.debug(`Waiting ${ms} milliseconds ...`) // debug is only output if you set the secret `ACTIONS_STEP_DEBUG` to true
|
const patterns = utils.getInputAsArray(Inputs.Patterns, {
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
const gitignore = utils.getInputAsBool(Inputs.Gitignore) || true
|
||||||
|
const ignoreFiles = utils.getInputAsArray(Inputs.IgnoreFiles)
|
||||||
|
core.debug(`workdir: ${workdir}`)
|
||||||
|
core.debug(`patterns: ${patterns}`)
|
||||||
|
core.debug(`gitignore: ${gitignore}`)
|
||||||
|
core.debug(`ignoreFiles: ${ignoreFiles}`)
|
||||||
|
|
||||||
core.debug(new Date().toTimeString())
|
const files = await getFiles(workdir, patterns, {gitignore, ignoreFiles})
|
||||||
await wait(parseInt(ms, 10))
|
let hash = ''
|
||||||
core.debug(new Date().toTimeString())
|
core.debug(files.toString())
|
||||||
|
const reads = files.map(async file => readFile(file))
|
||||||
core.setOutput('time', new Date().toTimeString())
|
const contents = await Promise.all(reads)
|
||||||
|
if (contents.length === 1) {
|
||||||
|
hash = contents[0]
|
||||||
|
} else if (contents.length > 1) {
|
||||||
|
let hashStr = ''
|
||||||
|
for (const content of contents) {
|
||||||
|
hashStr += content
|
||||||
|
}
|
||||||
|
hash = utils.hashHex(hashStr)
|
||||||
|
}
|
||||||
|
core.setOutput(Outputs.Hash, hash)
|
||||||
|
core.setOutput(Outputs.MatchedFiles, files)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) core.setFailed(error.message)
|
if (error instanceof Error) core.setFailed(error.message)
|
||||||
}
|
}
|
||||||
|
40
src/utils.ts
Normal file
40
src/utils.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import crypto, {BinaryLike} from 'crypto'
|
||||||
|
|
||||||
|
export function getInput(name: string, options?: core.InputOptions): string {
|
||||||
|
return core.getInput(name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInputAsArray(
|
||||||
|
name: string,
|
||||||
|
options?: core.InputOptions
|
||||||
|
): string[] {
|
||||||
|
return core
|
||||||
|
.getInput(name, options)
|
||||||
|
.split('\n')
|
||||||
|
.map(s => s.replace(/^!\s+/, '!').trim())
|
||||||
|
.filter(x => x !== '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInputAsInt(
|
||||||
|
name: string,
|
||||||
|
options?: core.InputOptions
|
||||||
|
): number | undefined {
|
||||||
|
const value = parseInt(core.getInput(name, options))
|
||||||
|
if (isNaN(value) || value < 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInputAsBool(
|
||||||
|
name: string,
|
||||||
|
options?: core.InputOptions
|
||||||
|
): boolean {
|
||||||
|
const result = core.getInput(name, options)
|
||||||
|
return result.toLowerCase() === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashHex(content: BinaryLike, shaAlgorithm = 'sha256'): string {
|
||||||
|
return crypto.createHash(shaAlgorithm).update(content).digest('hex')
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
export async function wait(milliseconds: number): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (isNaN(milliseconds)) {
|
|
||||||
throw new Error('milliseconds not a number')
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => resolve('done!'), milliseconds)
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user