mirror of
https://github.com/jimeh/update-tags-action.git
synced 2026-02-19 09:36:41 +00:00
feat(inputs): add dry run support (#76)
This commit is contained in:
@@ -10,6 +10,7 @@ export interface Inputs {
|
||||
defaultRef: string
|
||||
whenExists: WhenExistsMode
|
||||
annotation: string
|
||||
dryRun: boolean
|
||||
owner: string
|
||||
repo: string
|
||||
token: string
|
||||
@@ -43,6 +44,7 @@ export function getInputs(): Inputs {
|
||||
const whenExistsInput = core.getInput('when_exists') || 'update'
|
||||
const whenExists = validateWhenExists(whenExistsInput)
|
||||
const annotation: string = core.getInput('annotation')
|
||||
const dryRun: boolean = core.getBooleanInput('dry_run')
|
||||
const token: string = core.getInput('github_token', {
|
||||
required: true
|
||||
})
|
||||
@@ -63,6 +65,7 @@ export function getInputs(): Inputs {
|
||||
defaultRef,
|
||||
whenExists,
|
||||
annotation,
|
||||
dryRun,
|
||||
owner,
|
||||
repo,
|
||||
token
|
||||
|
||||
47
src/logger.ts
Normal file
47
src/logger.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as core from '@actions/core'
|
||||
import type { AnnotationProperties } from '@actions/core'
|
||||
|
||||
/**
|
||||
* Logger interface that mirrors @actions/core logging functions.
|
||||
*/
|
||||
export interface Logger {
|
||||
debug: (message: string) => void
|
||||
info: (message: string) => void
|
||||
notice: (message: string | Error, properties?: AnnotationProperties) => void
|
||||
warning: (message: string | Error, properties?: AnnotationProperties) => void
|
||||
error: (message: string | Error, properties?: AnnotationProperties) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logger that optionally prefixes all messages.
|
||||
*
|
||||
* @param prefix - Optional prefix to prepend to all log messages
|
||||
* @returns Logger instance with prefixed messages
|
||||
*/
|
||||
export function createLogger(prefix: string = ''): Logger {
|
||||
const prefixMessage = (msg: string | Error): string | Error => {
|
||||
if (!prefix) {
|
||||
return msg
|
||||
}
|
||||
if (typeof msg === 'string') {
|
||||
return `${prefix}${msg}`
|
||||
}
|
||||
// Wrap Error with prefixed message, preserving the original as cause
|
||||
const wrapped = new Error(`${prefix}${msg.message}`, { cause: msg })
|
||||
if (msg.stack) {
|
||||
wrapped.stack = msg.stack
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
|
||||
return {
|
||||
debug: (message: string) => core.debug(`${prefix}${message}`),
|
||||
info: (message: string) => core.info(`${prefix}${message}`),
|
||||
notice: (message: string | Error, properties?: AnnotationProperties) =>
|
||||
core.notice(prefixMessage(message), properties),
|
||||
warning: (message: string | Error, properties?: AnnotationProperties) =>
|
||||
core.warning(prefixMessage(message), properties),
|
||||
error: (message: string | Error, properties?: AnnotationProperties) =>
|
||||
core.error(prefixMessage(message), properties)
|
||||
}
|
||||
}
|
||||
21
src/main.ts
21
src/main.ts
@@ -14,20 +14,27 @@ export async function run(): Promise<void> {
|
||||
const octokit = github.getOctokit(inputs.token)
|
||||
const operations = await planTagOperations(inputs, octokit)
|
||||
|
||||
if (inputs.dryRun) {
|
||||
core.info('[dry-run] Dry-run mode enabled, no changes will be made.')
|
||||
}
|
||||
|
||||
const created: string[] = []
|
||||
const updated: string[] = []
|
||||
const skipped: string[] = []
|
||||
|
||||
// Execute all planned operations.
|
||||
for (const operation of operations) {
|
||||
await executeTagOperation(operation, octokit)
|
||||
await executeTagOperation(operation, octokit, { dryRun: inputs.dryRun })
|
||||
|
||||
if (operation.operation === 'create') {
|
||||
created.push(operation.name)
|
||||
} else if (operation.operation === 'update') {
|
||||
updated.push(operation.name)
|
||||
} else if (operation.operation === 'skip') {
|
||||
skipped.push(operation.name)
|
||||
// Only track actual changes when not in dry-run mode
|
||||
if (!inputs.dryRun) {
|
||||
if (operation.operation === 'create') {
|
||||
created.push(operation.name)
|
||||
} else if (operation.operation === 'update') {
|
||||
updated.push(operation.name)
|
||||
} else if (operation.operation === 'skip') {
|
||||
skipped.push(operation.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
87
src/tags.ts
87
src/tags.ts
@@ -1,6 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import type { Inputs } from './inputs.js'
|
||||
import { createLogger, type Logger } from './logger.js'
|
||||
|
||||
export interface ExistingTagInfo {
|
||||
commitSHA: string
|
||||
@@ -37,12 +37,21 @@ export interface SkipOperation extends BaseOperation {
|
||||
|
||||
export type TagOperation = CreateOperation | UpdateOperation | SkipOperation
|
||||
|
||||
interface Context {
|
||||
export interface ExecuteOptions {
|
||||
dryRun?: boolean
|
||||
}
|
||||
|
||||
interface ReadContext {
|
||||
owner: string
|
||||
repo: string
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
}
|
||||
|
||||
interface Context extends ReadContext {
|
||||
dryRun: boolean
|
||||
log: Logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan tag operations based on inputs.
|
||||
*
|
||||
@@ -92,7 +101,7 @@ export async function planTagOperations(
|
||||
}
|
||||
|
||||
// Pre-resolve all unique refs in parallel.
|
||||
const ctx: Context = { owner: inputs.owner, repo: inputs.repo, octokit }
|
||||
const ctx: ReadContext = { owner: inputs.owner, repo: inputs.repo, octokit }
|
||||
const refSHAs: Record<string, string> = {}
|
||||
await Promise.all(
|
||||
Array.from(uniqueRefs).map(async (ref) => {
|
||||
@@ -197,26 +206,24 @@ export async function planTagOperations(
|
||||
*
|
||||
* @param operation - The planned tag operation to execute
|
||||
* @param octokit - GitHub API client
|
||||
* @param options - Execution options (e.g., dryRun)
|
||||
*/
|
||||
export async function executeTagOperation(
|
||||
operation: TagOperation,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
octokit: ReturnType<typeof github.getOctokit>,
|
||||
options: ExecuteOptions = {}
|
||||
): Promise<void> {
|
||||
const dryRun = options.dryRun ?? false
|
||||
const ctx: Context = {
|
||||
owner: operation.owner,
|
||||
repo: operation.repo,
|
||||
octokit
|
||||
octokit,
|
||||
dryRun,
|
||||
log: createLogger(dryRun ? '[dry-run] ' : '')
|
||||
}
|
||||
|
||||
if (operation.operation === 'skip') {
|
||||
if (operation.reason === 'when_exists_skip') {
|
||||
core.info(`Tag '${operation.name}' exists, skipping.`)
|
||||
} else {
|
||||
core.info(
|
||||
`Tag '${operation.name}' already exists with desired commit SHA ${operation.sha}` +
|
||||
(operation.existingIsAnnotated ? ' (annotated).' : '.')
|
||||
)
|
||||
}
|
||||
logSkipOperation(ctx, operation)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,15 +242,30 @@ export async function executeTagOperation(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a skip operation.
|
||||
*/
|
||||
function logSkipOperation(ctx: Context, operation: SkipOperation): void {
|
||||
if (operation.reason === 'when_exists_skip') {
|
||||
ctx.log.info(`Tag '${operation.name}' exists, skipping.`)
|
||||
} else {
|
||||
ctx.log.info(
|
||||
`Tag '${operation.name}' already exists with desired ` +
|
||||
`commit SHA ${operation.sha}` +
|
||||
(operation.existingIsAnnotated ? ' (annotated).' : '.')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch information about an existing tag, dereferencing if annotated.
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param ctx - Read-only operation context
|
||||
* @param tagName - The name of the tag to fetch
|
||||
* @returns Information about the existing tag
|
||||
*/
|
||||
async function fetchTagInfo(
|
||||
ctx: Context,
|
||||
ctx: ReadContext,
|
||||
tagName: string
|
||||
): Promise<ExistingTagInfo> {
|
||||
const ref = await ctx.octokit.rest.git.getRef({
|
||||
@@ -278,11 +300,11 @@ async function fetchTagInfo(
|
||||
/**
|
||||
* Resolve a ref to a SHA.
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param ctx - Read-only operation context
|
||||
* @param ref - The ref to resolve
|
||||
* @returns The SHA
|
||||
*/
|
||||
async function resolveRefToSha(ctx: Context, ref: string): Promise<string> {
|
||||
async function resolveRefToSha(ctx: ReadContext, ref: string): Promise<string> {
|
||||
try {
|
||||
const {
|
||||
data: { sha }
|
||||
@@ -306,19 +328,24 @@ async function updateExistingTag(
|
||||
operation: UpdateOperation
|
||||
): Promise<void> {
|
||||
const commitMatches = operation.existingSHA === operation.sha
|
||||
const verb = ctx.dryRun ? 'Would update' : 'Updating'
|
||||
|
||||
if (commitMatches) {
|
||||
core.info(
|
||||
`Tag '${operation.name}' exists with same commit but ${operation.reasons.join(', ')}.`
|
||||
ctx.log.info(
|
||||
`${verb} tag '${operation.name}', ${operation.reasons.join(', ')}.`
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
`Tag '${operation.name}' exists` +
|
||||
`${operation.existingIsAnnotated ? ' (annotated)' : ''}` +
|
||||
`, updating to ${operation.reasons.join(', ')}.`
|
||||
ctx.log.info(
|
||||
`${verb} tag '${operation.name}'` +
|
||||
`${operation.existingIsAnnotated ? ' (annotated)' : ''} ` +
|
||||
`to ${operation.reasons.join(', ')}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (ctx.dryRun) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetSha = await resolveTargetSHA(
|
||||
ctx,
|
||||
operation.name,
|
||||
@@ -342,10 +369,16 @@ async function createTag(
|
||||
ctx: Context,
|
||||
operation: CreateOperation
|
||||
): Promise<void> {
|
||||
core.info(
|
||||
`Tag '${operation.name}' does not exist, creating with commit SHA ${operation.sha}.`
|
||||
const verb = ctx.dryRun ? 'Would create' : 'Creating'
|
||||
ctx.log.info(
|
||||
`${verb} tag '${operation.name}' at commit SHA ${operation.sha}` +
|
||||
(operation.annotation ? ' (annotated).' : '.')
|
||||
)
|
||||
|
||||
if (ctx.dryRun) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetSha = await resolveTargetSHA(
|
||||
ctx,
|
||||
operation.name,
|
||||
@@ -364,14 +397,14 @@ async function createTag(
|
||||
/**
|
||||
* Resolve the target SHA for a tag (creates annotated tag object if needed).
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param ctx - Read-only operation context
|
||||
* @param tagName - The tag name
|
||||
* @param commitSha - The commit SHA
|
||||
* @param annotation - The annotation message (if any)
|
||||
* @returns The SHA to use (tag object SHA if annotated, commit SHA otherwise)
|
||||
*/
|
||||
async function resolveTargetSHA(
|
||||
ctx: Context,
|
||||
ctx: ReadContext,
|
||||
tagName: string,
|
||||
commitSha: string,
|
||||
annotation: string
|
||||
|
||||
Reference in New Issue
Block a user