init
This commit is contained in:
175
.gitignore
vendored
Normal file
175
.gitignore
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
14
BUILDING.md
Normal file
14
BUILDING.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Building
|
||||
|
||||
Test locally using [Act](https://github.com/nektos/act)
|
||||
|
||||
## Update `v1` tag
|
||||
|
||||
`v1` should be always the latest version of `v1.x.x`
|
||||
|
||||
```console
|
||||
git tag -d v1
|
||||
git push --delete origin v1
|
||||
git tag v1
|
||||
git push --tags
|
||||
```
|
||||
73
README.md
Normal file
73
README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# checksum
|
||||
|
||||
This action calculates the checksum (e.g., SHA-256) for all assets in your GitHub releases and generates a `checksum.txt` file. This is useful for ensuring file integrity during downloads and uploads.
|
||||
|
||||
## Usage
|
||||
|
||||
Include in your workflow file:
|
||||
|
||||
```yml
|
||||
uses: thewh1teagle/checksum@v1
|
||||
with:
|
||||
patterns: | # Optional
|
||||
*.zip
|
||||
*.tar.gz
|
||||
algorithm: sha256 # Optional. See bun.sh/docs/api/hashing#bun-cryptohasher for supported algorithms
|
||||
```
|
||||
|
||||
Each row separated by tab. (`\t`)
|
||||
|
||||
You must enable write permission in github.com/user/repo/settings/actions -> Workflow permissions -> Read and write permissions.
|
||||
|
||||
## Inputs
|
||||
|
||||
| **Input Name** | **Description** | **Required** | **Default** |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---------------- |
|
||||
| `repo` | The GitHub repository in the format `owner/repo`. | No | |
|
||||
| `patterns` | File patterns to run checksum on. Supports glob patterns like `*.zip`, `*.tar.gz`. | No | `""` (all files) |
|
||||
| `algorithm` | Hash algorithm to use. Defaults to `sha256`. See [Bun hashing documentation](https://bun.sh/docs/api/hashing#bun-cryptohasher). | No | `sha256` |
|
||||
| `pre-release` | Whether to run on pre-releases. | No | `false` |
|
||||
| `tag` | The tag of the release to generate checksums for. | No | `''` |
|
||||
| `file-name` | The name of the checksum file to generate. | No | `checksum.txt` |
|
||||
| `dry-run` | Run without upload. will be available in the console output of the action. | No | `checksum.txt` |
|
||||
| `bun-version` | The version of Bun to use. | No | `latest` |
|
||||
|
||||
## Example checksum.txt
|
||||
|
||||
```txt
|
||||
a.txt ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb
|
||||
b.txt 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f
|
||||
```
|
||||
|
||||
## Full example
|
||||
|
||||
<details>
|
||||
|
||||
```yml
|
||||
name: Create checksum.txt
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * *" # Runs at 1:00 AM UTC daily
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Run checksum action
|
||||
uses: thewh1teagle/checksum@v1
|
||||
with:
|
||||
patterns: | # Optional
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.txt
|
||||
!b.txt
|
||||
algorithm: sha256 # Optional
|
||||
env:
|
||||
# You must enable write permission in github.com/user/repo/settings/actions -> Workflow permissions -> Read and write permissions
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
</details>
|
||||
60
action.yml
Normal file
60
action.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
name: "Checksums Action"
|
||||
author: thewh1teagle
|
||||
branding:
|
||||
icon: 'check-circle' # Optional, you can add a custom icon
|
||||
color: 'blue'
|
||||
description: "A reusable action to create checksum.txt"
|
||||
|
||||
inputs:
|
||||
bun-version:
|
||||
description: "The version of Bun to use"
|
||||
required: false
|
||||
default: "latest"
|
||||
repo:
|
||||
description: "The GitHub repository (owner/repo)"
|
||||
required: false
|
||||
patterns:
|
||||
description: "Patterns for the files to run checksum on"
|
||||
required: false
|
||||
default: ""
|
||||
algorithm:
|
||||
description: "Hash algorithm to use. default to sha256. See https://bun.sh/docs/api/hashing#bun-cryptohasher"
|
||||
required: false
|
||||
default: "sha256"
|
||||
pre-release:
|
||||
description: "Run on pre release"
|
||||
required: false
|
||||
default: 'false'
|
||||
tag:
|
||||
description: "Tag of the release"
|
||||
required: false
|
||||
default: ''
|
||||
file-name:
|
||||
description: "File name of the checksum file"
|
||||
required: false
|
||||
default: 'checksum.txt'
|
||||
dry-run:
|
||||
description: "Create checksum without upload. will be available in the output of the action run"
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ inputs.bun-version }}
|
||||
|
||||
- name: Create checksum.txt
|
||||
run: bun run "${GITHUB_ACTION_PATH}/index.ts"
|
||||
shell: bash
|
||||
# env: ${{ toJson(vars) }}
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
INPUT_PATTERNS: ${{ inputs.patterns }}
|
||||
INPUT_ALGORITHM: ${{ inputs.algorithm }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
INPUT_FILE_NAME: ${{ inputs.file-name }}
|
||||
INPUT_PRE_RELEASE: ${{ inputs.pre-release }}
|
||||
INPUT_REPO: ${{ inputs.repo }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry-run }}
|
||||
195
index.ts
Normal file
195
index.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { $, Glob, type SupportedCryptoAlgorithms } from "bun";
|
||||
import { unlink } from "fs/promises";
|
||||
import { stat } from "fs/promises";
|
||||
import { appendFile } from "fs/promises";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
interface Asset {
|
||||
apiUrl: string;
|
||||
contentType: string;
|
||||
createdAt: string;
|
||||
downloadCount: number;
|
||||
id: string;
|
||||
label: string;
|
||||
name: string;
|
||||
size: number;
|
||||
state: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
async function getReleaseNoAuth(repo: string, tag: string) {
|
||||
const url = `https://api.github.com/repos/${repo}/releases/tags/${tag}`;
|
||||
const response = await fetch(url, {
|
||||
headers: { Accept: "application/vnd.github.v3+json" },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch release details: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
return data;
|
||||
}
|
||||
|
||||
function parsePatternsInput(patternsInput: string): string[] {
|
||||
return patternsInput
|
||||
.split("\n")
|
||||
.map((pattern) => pattern.replace(/^"|"$/g, "").trim())
|
||||
.filter((pattern) => pattern !== "");
|
||||
}
|
||||
|
||||
async function downloadAsset(asset: Asset): Promise<void> {
|
||||
console.log(`Downloading ${asset.name}...`);
|
||||
await $`wget ${asset.url} -O ${asset.name}`;
|
||||
}
|
||||
|
||||
async function generateChecksum(assetName: string): Promise<string> {
|
||||
const fileBuffer = await readFile(assetName);
|
||||
const hasher = new Bun.CryptoHasher(
|
||||
hashAlgorithm as SupportedCryptoAlgorithms
|
||||
);
|
||||
const fileBlob = new Blob([fileBuffer]);
|
||||
hasher.update(fileBlob);
|
||||
return hasher.digest("hex");
|
||||
}
|
||||
|
||||
function shouldIncludeAsset(assetName: string, patterns: string[]) {
|
||||
let matched = false;
|
||||
let excluded = false;
|
||||
|
||||
// Check for each pattern
|
||||
for (const pattern of patterns) {
|
||||
const glob = new Glob(pattern);
|
||||
|
||||
if (pattern.startsWith("!") && !glob.match(assetName)) {
|
||||
// This is an exclusion pattern
|
||||
excluded = true; // Mark as excluded
|
||||
break; // No need to check further patterns if excluded
|
||||
} else if (glob.match(assetName)) {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Excluded
|
||||
if (excluded) {
|
||||
return false;
|
||||
}
|
||||
// Matched and not excluded
|
||||
if (matched) {
|
||||
return true;
|
||||
}
|
||||
// Not matched
|
||||
return false;
|
||||
}
|
||||
|
||||
async function uploadChecksumFile(
|
||||
checkSumPath: string,
|
||||
repo: string,
|
||||
tag: string
|
||||
): Promise<void> {
|
||||
if (isDryRun) {
|
||||
console.info(
|
||||
`Dry run. Skipping upload of ${checkSumPath} to ${repo}@${tag}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const fileInfo = await stat(checkSumPath);
|
||||
if (fileInfo.size === 0) {
|
||||
console.warn("Checksum is empty. Skipping upload.");
|
||||
} else {
|
||||
console.log("Uploading checksum file.");
|
||||
await $`gh release upload -R ${repo} ${tag} ${checkSumPath} --clobber`;
|
||||
}
|
||||
}
|
||||
|
||||
// Info before we start
|
||||
|
||||
console.log("Starting checksum action...");
|
||||
console.log(`Checksum file name: ${process.env.INPUT_FILE_NAME}`);
|
||||
console.log(`Hash Algorithm: ${process.env.INPUT_ALGORITHM || "sha256"}`);
|
||||
console.log(`Repo: ${process.env.INPUT_REPO}`);
|
||||
console.log(`Tag Input: ${process.env.INPUT_TAG}`);
|
||||
console.log(`Patterns Input: ${process.env.INPUT_PATTERNS}`);
|
||||
console.log(`Dry Run: ${process.env.INPUT_DRY_RUN}`);
|
||||
|
||||
// Constants
|
||||
const checkSumPath = process.env.INPUT_FILE_NAME!; // checksum.txt by default
|
||||
const minSizeImmediateUpload = 1000 * 1000 * 500; // 500MB
|
||||
const isDryRun = process.env.INPUT_DRY_RUN === 'true'
|
||||
const isPreRelease = process.env.INPUT_PRE_RELEASE === "true";
|
||||
|
||||
// Algorithm
|
||||
const hashAlgorithm = process.env.INPUT_ALGORITHM || "sha256";
|
||||
|
||||
// Repo
|
||||
const githubContext = JSON.parse(process.env.GITHUB_CONTEXT ?? "{}");
|
||||
const repo = process.env.INPUT_REPO || githubContext.repository;
|
||||
|
||||
// Tag
|
||||
const tagInput = process.env.INPUT_TAG;
|
||||
let tag = "";
|
||||
if (tagInput) {
|
||||
// Specified tag
|
||||
tag = tagInput.trim();
|
||||
} else {
|
||||
// Get latest tag
|
||||
|
||||
|
||||
// Pre release
|
||||
if (isPreRelease) {
|
||||
// https://github.com/cli/cli/issues/9909#issuecomment-2473608076
|
||||
tag =
|
||||
await $`gh release list -R ${repo} --json isPrerelease,tagName --jq 'map(select(.isPrerelease)) | first | .tagName'`
|
||||
.text()
|
||||
.then((t) => t.trim());
|
||||
} else {
|
||||
tag = await $`gh release view -R ${repo} --json tagName --jq .tagName`
|
||||
.text()
|
||||
.then((t) => t.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Patterns
|
||||
const patterns = parsePatternsInput(process.env.INPUT_PATTERNS ?? "");
|
||||
|
||||
let releaseContent: string;
|
||||
if (isDryRun) {
|
||||
releaseContent = await getReleaseNoAuth(repo, tag);
|
||||
} else {
|
||||
releaseContent = await $`gh release view ${tag} -R ${repo} --json assets`.text();
|
||||
}
|
||||
const releases: { assets: Asset[] } = JSON.parse(releaseContent);
|
||||
|
||||
for (const asset of releases.assets) {
|
||||
if (asset.name == checkSumPath) {
|
||||
console.info("Found existing checksum in assets");
|
||||
continue;
|
||||
}
|
||||
if (patterns.length && !shouldIncludeAsset(asset.name, patterns)) {
|
||||
console.log(`Skip ${asset.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await downloadAsset(asset);
|
||||
|
||||
const checksum = await generateChecksum(asset.name);
|
||||
await unlink(asset.name); // Remove file immediately after get checksum
|
||||
await appendFile(checkSumPath, `${asset.name}\t${checksum}\n`);
|
||||
|
||||
if (asset.size > minSizeImmediateUpload) {
|
||||
console.log(`Uploading immdiately due to large file: ${asset.name}`);
|
||||
await uploadChecksumFile(checkSumPath, repo, tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Final upload of the checksum file
|
||||
await uploadChecksumFile(checkSumPath, repo, tag);
|
||||
|
||||
// Info
|
||||
console.log(
|
||||
`Release Tag: ${tag}, Repo: ${repo}, Algorithm: ${hashAlgorithm}, Patterns: ${JSON.stringify(
|
||||
patterns
|
||||
)}, Assets processed: ${releases.assets.length}`
|
||||
);
|
||||
console.log(`Checksum file content:\n${await readFile(checkSumPath, "utf-8")}`);
|
||||
11
package.json
Normal file
11
package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "checksum",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user