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:
commit
13ce75aa4d
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
lib/
|
||||
dist/
|
||||
node_modules/
|
||||
coverage/
|
103
.gitignore
vendored
Normal file
103
.gitignore
vendored
Normal 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
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
coverage/
|
16
.prettierrc.json
Normal file
16
.prettierrc.json
Normal 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
58
README.md
Normal 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
19
action.yml
Normal 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
4325
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal 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
204
src/index.ts
Normal 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
19
tsconfig.json
Normal 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"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user