fix: normalize refs-tag inputs (#755)

Signed-off-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
Rui Chen
2026-03-15 00:05:22 -04:00
committed by GitHub
parent 21ae1a1eb2
commit 45211baa90
7 changed files with 103 additions and 29 deletions

View File

@@ -189,7 +189,7 @@ The following are optional as `step.with` keys
| `files` | String | Newline-delimited globs of paths to assets to upload for release. Escape glob metacharacters when you need to match a literal filename that contains them, such as `[` or `]`. On Windows, both `\` and `/` separators are accepted. |
| `overwrite_files` | Boolean | Indicator of whether files should be overwritten when they already exist. Defaults to true |
| `name` | String | Name of the release. defaults to tag name |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name`. `refs/tags/<name>` values are normalized to `<name>`. |
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
| `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable |
| `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. |

View File

@@ -498,6 +498,58 @@ describe('github', () => {
assert.equal(result.created, false);
});
it('normalizes refs/tags-prefixed input_tag_name values before reusing an existing release', async () => {
const existingRelease: Release = {
id: 1,
upload_url: 'test',
html_url: 'test',
tag_name: 'v1.0.0',
name: 'test',
body: 'test',
target_commitish: 'main',
draft: false,
prerelease: false,
assets: [],
};
const updateReleaseSpy = vi.fn(async () => ({ data: existingRelease }));
const getReleaseByTagSpy = vi.fn(async () => ({ data: existingRelease }));
const result = await release(
{
...config,
input_tag_name: 'refs/tags/v1.0.0',
},
{
getReleaseByTag: getReleaseByTagSpy,
createRelease: () => Promise.reject('Not implemented'),
updateRelease: updateReleaseSpy,
finalizeRelease: () => Promise.reject('Not implemented'),
allReleases: async function* () {
yield { data: [existingRelease] };
},
listReleaseAssets: () => Promise.reject('Not implemented'),
deleteReleaseAsset: () => Promise.reject('Not implemented'),
deleteRelease: () => Promise.reject('Not implemented'),
updateReleaseAsset: () => Promise.reject('Not implemented'),
uploadReleaseAsset: () => Promise.reject('Not implemented'),
},
1,
);
expect(getReleaseByTagSpy).toHaveBeenCalledWith({
owner: 'owner',
repo: 'repo',
tag: 'v1.0.0',
});
expect(updateReleaseSpy).toHaveBeenCalledWith(
expect.objectContaining({
tag_name: 'v1.0.0',
}),
);
assert.equal(result.release.id, existingRelease.id);
assert.equal(result.created, false);
});
it('reuses a canonical release after concurrent create success and removes empty duplicates', async () => {
const canonicalRelease: Release = {
id: 1,

View File

@@ -2,6 +2,7 @@ import {
alignAssetName,
isTag,
normalizeGlobPattern,
normalizeTagName,
parseConfig,
parseInputFiles,
paths,
@@ -469,6 +470,10 @@ describe('util', () => {
},
);
});
it('normalizes refs/tags-prefixed input_tag_name values', () => {
expect(parseConfig({ INPUT_TAG_NAME: 'refs/tags/v1.2.3' }).input_tag_name).toBe('v1.2.3');
});
});
describe('isTag', () => {
it('returns true for tags', async () => {
@@ -479,6 +484,16 @@ describe('util', () => {
});
});
describe('normalizeTagName', () => {
it('strips refs/tags/ from explicit tag names', () => {
assert.equal(normalizeTagName('refs/tags/v1.2.3'), 'v1.2.3');
});
it('leaves plain tag names unchanged', () => {
assert.equal(normalizeTagName('v1.2.3'), 'v1.2.3');
});
});
describe('paths', () => {
it('resolves files given a set of paths', async () => {
assert.deepStrictEqual(paths(['tests/data/**/*', 'tests/data/does/not/exist/*']), [

View File

@@ -13,7 +13,7 @@ inputs:
description: "Gives the release a custom name. Defaults to tag name"
required: false
tag_name:
description: "Gives a tag name. Defaults to github.ref_name"
description: "Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>."
required: false
draft:
description: "Creates a draft release. Defaults to false"

48
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@ import { statSync } from 'fs';
import { open } from 'fs/promises';
import { lookup } from 'mime-types';
import { basename } from 'path';
import { alignAssetName, Config, isTag, releaseBody } from './util';
import { alignAssetName, Config, isTag, normalizeTagName, releaseBody } from './util';
type GitHub = InstanceType<typeof GitHub>;
@@ -420,7 +420,7 @@ export const release = async (
const [owner, repo] = config.github_repository.split('/');
const tag =
config.input_tag_name ||
normalizeTagName(config.input_tag_name) ||
(isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : '');
const discussion_category_name = config.input_discussion_category_name;

View File

@@ -98,7 +98,7 @@ export const parseConfig = (env: Env): Config => {
github_ref: env.GITHUB_REF || '',
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || '',
input_name: env.INPUT_NAME,
input_tag_name: env.INPUT_TAG_NAME?.trim(),
input_tag_name: normalizeTagName(env.INPUT_TAG_NAME?.trim()),
input_body: env.INPUT_BODY,
input_body_path: env.INPUT_BODY_PATH,
input_files: parseInputFiles(env.INPUT_FILES || ''),
@@ -170,6 +170,13 @@ export const isTag = (ref: string): boolean => {
return ref.startsWith('refs/tags/');
};
export const normalizeTagName = (tag: string | undefined): string | undefined => {
if (!tag) {
return tag;
}
return isTag(tag) ? tag.replace('refs/tags/', '') : tag;
};
export const alignAssetName = (assetName: string): string => {
return assetName.replace(/ /g, '.');
};