6
mirror of https://github.com/AllSpiceIO/notes-kv.git synced 2025-04-18 05:09:17 +00:00

Initialize with v0.1

This commit is contained in:
Shrikanth Upadhayaya 2024-07-15 10:35:58 -04:00
commit 13ce75aa4d
No known key found for this signature in database
10 changed files with 4777 additions and 0 deletions

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
lib/
dist/
node_modules/
coverage/

103
.gitignore vendored Normal file
View File

@ -0,0 +1,103 @@
# Dependency directory
node_modules
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# 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
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
__tests__/runner/*
# IDE files
.idea
.vscode
*.code-workspace

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
dist/
node_modules/
coverage/

16
.prettierrc.json Normal file
View File

@ -0,0 +1,16 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": true,
"arrowParens": "avoid",
"proseWrap": "always",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}

58
README.md Normal file
View File

@ -0,0 +1,58 @@
# Notes-KV
A GitHub/Gitea/AllSpice Hub action to use git-notes as a Key/Value store.
## Usage
```yaml
jobs:
save-metadata:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure git user
run: |
git config --global user.email "<your email here>"
git config --global user.name "<your name here>"
- name: Save metadata to git notes
uses: AllSpiceIO/notes-kv@v0.1
with:
values: |
build_number=${{ github.run_number }}
commit_sha=${{ github.sha }}
author=${{ github.actor }}
```
This action automatically pulls notes from the `origin` remote and pushes them
back after changes. If there are already key/values stored in the notes, they
will be merged with the new values.
This action uses a custom ref, `notes-kv` to avoid conflicts with other notes.
If you want to use your own ref, you can set it using `custom_ref`:
```yaml
- name: Save metadata to git notes
uses: AllSpiceIO/notes-kv@v0.1
with:
custom_ref: my-notes
values: |
build_number=${{ github.run_number }}
commit_sha=${{ github.sha }}
author=${{ github.actor }}
```
For more information on git notes, refer to the [Git documentation](https://git-scm.com/docs/git-notes).
## Caveats
- The git user should be configured _before_ this action is run:
```sh
git config --global user.email "demo@example.org"
git config --global user.name "Demo"
```
- You should be able to push to the repository without entering a password.
- If you have multiple runs at the same time, this action has a potential race
condition where the notes could be overwritten by another run.

19
action.yml Normal file
View File

@ -0,0 +1,19 @@
name: "notes-kv"
description: "Save key/value pairs to git notes"
inputs:
values:
description:
"The key/value pairs to save, listed as <key>=<value>. Either this or the
values_file must be present."
required: false
values_file:
description:
"A JSON file of the key/value pairs. Either this or values must be
present."
required: false
custom_ref:
description: "The ref used to store the k/v notes. Defaults to notes-kv."
required: false
runs:
using: "node20"
main: "dist/index.js"

4325
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "notes-kv",
"version": "0.1.0",
"description": "An actions add-on to save Key/Value pairs to git notes",
"main": "dist/index.js",
"scripts": {
"package": "npx esbuild src/index.ts --bundle --platform=node --target=node20 --outdir=dist",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "AllSpice Inc. <maintainers@allspice.io>",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@types/node": "^20.14.10",
"esbuild": "^0.23.0",
"eslint": "^8.57.0",
"eslint-plugin-github": "^5.0.1",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.2",
"prettier-eslint": "^16.3.0",
"typescript": "^5.5.3"
}
}

204
src/index.ts Normal file
View File

@ -0,0 +1,204 @@
import { getInput, info, warning, setFailed, debug } from "@actions/core"
import { readFile } from "node:fs/promises"
import { exec } from "@actions/exec"
/**
* The name of the ref used to store KV notes.
*/
const CUSTOM_NOTES_REF = "notes-kv"
/**
* Parses input strings of the form "key1=value1\nkey2=value2" into a key-value object.
* @param input - The input string to parse.
* @returns A key-value object.
* @throws If the input is not in the expected format.
*/
const parseInput = (input: string): Record<string, string> => {
const pairs = input
.trim()
.split("\n")
.map(line =>
line
.trim()
.split("=")
.map(part => part.trim()),
)
const invalidLines = pairs.filter(
pair => pair.length !== 2 || !pair[0] || !pair[1],
)
if (invalidLines.length > 0) {
debug("Input: " + input)
throw new Error(
`Invalid input format on lines: ${invalidLines.map((_, i) => i + 1).join(", ")}`,
)
}
return Object.fromEntries(pairs)
}
/**
* Reads the input values from the provided values or values_file input.
* @param valuesInput - The values input string.
* @param valuesFile - The values file path.
* @returns The key-value object parsed from the input.
* @throws If both values and values_file are provided, if neither are
* provided, if the values file cannot be read, or if the input is invalid.
*/
const readInput = async (
valuesInput: string,
valuesFile: string,
): Promise<Record<string, any>> => {
if (
valuesInput &&
valuesInput.trim() !== "" &&
valuesFile &&
valuesFile.trim() !== ""
) {
throw new Error("Both values and values_file cannot be provided.")
}
if (valuesInput && valuesInput.trim() !== "") {
return parseInput(valuesInput)
}
if (valuesFile && valuesFile.trim() !== "") {
const content = await readFile(valuesFile, "utf8")
const json_object = JSON.parse(content)
// We can't store an array or a primitive; we need an object.
if (typeof json_object !== "object") {
throw new Error("Input must be a JSON object.")
}
return json_object
}
throw new Error("Either values or values_file must be provided.")
}
/**
* Executes a git command and returns the output on STDOUT.
* @param args - The arguments to pass to the git command.
* @returns The output of the git command.
* @throws If the git command fails.
*/
const execGit = async (...args: string[]): Promise<string> => {
let output = ""
const options = {
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
},
},
}
const code = await exec("git", args, options)
if (code !== 0) {
throw new Error(`Failed to run git ${args.join(" ")}`)
}
return output.trim()
}
const getCurrentCommit = async (): Promise<string> => {
return await execGit("rev-parse", "HEAD")
}
const fetchNotesRef = async (notesRef: string): Promise<void> => {
try {
await execGit(
"fetch",
"origin",
`refs/notes/${notesRef}:refs/notes/${notesRef}`,
)
} catch (error) {
const message =
error instanceof Error
? error.message
: "Unknown error occurred while fetching notes"
info(
`Note: ${message}. This may be normal when adding notes for the first time.`,
)
}
}
const addOrUpdateNotes = async (
notes: Record<string, string>,
commitSha: string,
notesRef: string,
): Promise<void> => {
let existingNote = ""
try {
existingNote = await execGit("notes", "--ref", notesRef, "show")
info("Existing note found. Preparing to update.")
} catch (error) {
info("No existing note found. A new note will be created.")
}
if (existingNote) {
try {
const existingNotes = JSON.parse(existingNote)
notes = { ...existingNotes, ...notes }
info(`Merged notes: ${JSON.stringify(notes)}`)
} catch (error) {
warning(
"Failed to parse existing note as JSON. Overwriting with new note.",
)
}
}
await execGit(
"notes",
"--ref",
notesRef,
"add",
"-f",
"-m",
JSON.stringify(notes),
commitSha,
)
info("Note added or updated successfully.")
}
const pushNotes = async (): Promise<void> => {
await execGit("push", "origin", `refs/notes/*`, "-f")
}
const run = async () => {
try {
const values = getInput("values")
const valuesFile = getInput("values_file")
const notesRefInput = getInput("custom_ref")
const notes = await readInput(values, valuesFile)
info(`Notes to store: ${JSON.stringify(notes)}`)
if (Object.keys(notes).length === 0) {
info("No values provided. Skipping note storage.")
return
}
const notesRef =
notesRefInput && notesRefInput.trim() !== ""
? notesRefInput
: CUSTOM_NOTES_REF
const commitSha = await getCurrentCommit()
await fetchNotesRef(notesRef)
await addOrUpdateNotes(notes, commitSha, notesRef)
await pushNotes()
info("Notes stored successfully.")
} catch (error) {
if (error instanceof Error) {
setFailed(error.message)
} else {
setFailed(`An unknown error occurred: ${error}`)
}
}
}
run()

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"rootDir": "./src",
"moduleResolution": "NodeNext",
"baseUrl": "./",
"sourceMap": true,
"outDir": "./dist",
"noImplicitAny": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"newLine": "lf"
},
"exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"]
}