This commit is contained in:
thewh1teagle
2024-12-20 02:40:08 +02:00
commit e901474b0f
8 changed files with 555 additions and 0 deletions

175
.gitignore vendored Normal file
View 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
View 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
View 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
View 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 }}

BIN
bun.lockb Executable file

Binary file not shown.

195
index.ts Normal file
View 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
View 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
View 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
}
}