mirror of
https://github.com/jimeh/update-tags-action.git
synced 2026-02-19 09:36:41 +00:00
feat(tags/derive): add semver tag derivation functionality (#79)
This commit is contained in:
105
src/derive.ts
Normal file
105
src/derive.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { parse } from 'csv-parse/sync'
|
||||
import Handlebars from 'handlebars'
|
||||
import * as semver from 'semver'
|
||||
|
||||
/**
|
||||
* Context containing parsed semver components for template rendering.
|
||||
*/
|
||||
export interface SemverContext {
|
||||
/** "v" or "V" if input had a prefix, empty string otherwise */
|
||||
prefix: string
|
||||
/** Major version number */
|
||||
major: number
|
||||
/** Minor version number */
|
||||
minor: number
|
||||
/** Patch version number */
|
||||
patch: number
|
||||
/** Prerelease identifier (e.g., "beta.1"), empty if none */
|
||||
prerelease: string
|
||||
/** Build metadata (e.g., "build.123"), empty if none */
|
||||
build: string
|
||||
/** Full version string without prefix */
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a version string into semver components.
|
||||
*
|
||||
* @param input - Version string (e.g., "v1.2.3", "1.2.3-beta.1+build.456")
|
||||
* @returns Parsed semver context
|
||||
* @throws Error if the version string is not valid semver
|
||||
*/
|
||||
export function parseSemver(input: string): SemverContext {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
throw new Error('Invalid semver: empty string')
|
||||
}
|
||||
|
||||
// Check for v/V prefix and preserve original case
|
||||
const firstChar = trimmed[0]
|
||||
const hasPrefix = firstChar === 'v' || firstChar === 'V'
|
||||
const prefix = hasPrefix ? firstChar : ''
|
||||
const versionStr = hasPrefix ? trimmed.slice(1) : trimmed
|
||||
|
||||
// Parse with semver library
|
||||
const parsed = semver.parse(versionStr)
|
||||
if (!parsed) {
|
||||
throw new Error(`Invalid semver: '${input}'`)
|
||||
}
|
||||
|
||||
return {
|
||||
prefix,
|
||||
major: parsed.major,
|
||||
minor: parsed.minor,
|
||||
patch: parsed.patch,
|
||||
prerelease: parsed.prerelease.join('.'),
|
||||
build: parsed.build.join('.'),
|
||||
version: parsed.version
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single template string with semver context using Handlebars.
|
||||
* Supports {{#if variable}}...{{/if}} conditionals for optional sections.
|
||||
*
|
||||
* @param template - Handlebars template string
|
||||
* @param ctx - Semver context for substitution
|
||||
* @returns Rendered string
|
||||
*/
|
||||
export function renderTemplate(template: string, ctx: SemverContext): string {
|
||||
const compiled = Handlebars.compile(template, { noEscape: true })
|
||||
// Convert numbers to strings so 0 is truthy in conditionals
|
||||
const stringCtx = {
|
||||
...ctx,
|
||||
major: String(ctx.major),
|
||||
minor: String(ctx.minor),
|
||||
patch: String(ctx.patch)
|
||||
}
|
||||
return compiled(stringCtx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive tags from a semver version string using a template.
|
||||
*
|
||||
* @param deriveFrom - Semver version string (e.g., "v1.2.3")
|
||||
* @param template - CSV/newline-delimited Handlebars template string
|
||||
* @returns Array of derived tag strings
|
||||
*/
|
||||
export function deriveTags(deriveFrom: string, template: string): string[] {
|
||||
const ctx = parseSemver(deriveFrom)
|
||||
|
||||
// Render template with Handlebars first, enabling conditional tag inclusion
|
||||
const rendered = renderTemplate(template, ctx)
|
||||
|
||||
// Parse rendered result as CSV/newline delimited
|
||||
const tags = (
|
||||
parse(rendered, {
|
||||
delimiter: ',',
|
||||
trim: true,
|
||||
relax_column_count: true
|
||||
}) as string[][]
|
||||
).flat()
|
||||
|
||||
// Exclude empty tags and tags that are just the prefix with no version data
|
||||
return tags.filter((tag) => tag.length > 0 && tag !== ctx.prefix)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { parse } from 'csv-parse/sync'
|
||||
import { deriveTags } from './derive.js'
|
||||
|
||||
const WHEN_EXISTS_MODES = ['update', 'skip', 'fail'] as const
|
||||
export type WhenExistsMode = (typeof WHEN_EXISTS_MODES)[number]
|
||||
@@ -33,13 +34,19 @@ function validateWhenExists(input: string): WhenExistsMode {
|
||||
return input as WhenExistsMode
|
||||
}
|
||||
|
||||
const DEFAULT_DERIVE_FROM_TEMPLATE =
|
||||
'{{prefix}}{{major}},{{prefix}}{{major}}.{{minor}}'
|
||||
|
||||
/**
|
||||
* Get and validate action inputs.
|
||||
*
|
||||
* @returns Parsed and validated inputs
|
||||
*/
|
||||
export function getInputs(): Inputs {
|
||||
const tagsInput: string = core.getInput('tags', { required: true })
|
||||
const tagsInput: string = core.getInput('tags')
|
||||
const deriveFrom: string = core.getInput('derive_from')
|
||||
const deriveFromTemplate: string =
|
||||
core.getInput('derive_from_template') || DEFAULT_DERIVE_FROM_TEMPLATE
|
||||
const defaultRef: string = core.getInput('ref')
|
||||
const whenExistsInput = core.getInput('when_exists') || 'update'
|
||||
const whenExists = validateWhenExists(whenExistsInput)
|
||||
@@ -51,14 +58,31 @@ export function getInputs(): Inputs {
|
||||
|
||||
const { owner, repo } = github.context.repo
|
||||
|
||||
// Parse tags as CSV/newline delimited strings
|
||||
const tags = (
|
||||
parse(tagsInput, {
|
||||
delimiter: ',',
|
||||
trim: true,
|
||||
relax_column_count: true
|
||||
}) as string[][]
|
||||
).flat()
|
||||
// Parse explicit tags as CSV/newline delimited strings
|
||||
const explicitTags: string[] = tagsInput
|
||||
? (
|
||||
parse(tagsInput, {
|
||||
delimiter: ',',
|
||||
trim: true,
|
||||
relax_column_count: true
|
||||
}) as string[][]
|
||||
).flat()
|
||||
: []
|
||||
|
||||
// Derive tags from semver version string if provided
|
||||
const derivedTags: string[] = deriveFrom
|
||||
? deriveTags(deriveFrom, deriveFromTemplate)
|
||||
: []
|
||||
|
||||
// Combine explicit and derived tags
|
||||
const tags = [...explicitTags, ...derivedTags]
|
||||
|
||||
// Validate that at least one tag source is provided
|
||||
if (tags.length === 0) {
|
||||
throw new Error(
|
||||
"No tags specified. Provide 'tags' input, 'derive_from' input, or both."
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
tags,
|
||||
|
||||
Reference in New Issue
Block a user