mirror of
https://github.com/jimeh/update-tags-action.git
synced 2026-02-19 01:26:40 +00:00
wip: further support for parsing input version and templated tags
This commit is contained in:
16
README.md
16
README.md
@@ -78,13 +78,15 @@ jobs:
|
||||
|
||||
## Inputs
|
||||
|
||||
| parameter | description | required | default |
|
||||
| ------------- | --------------------------------------------------------------------------------- | -------- | ------------------- |
|
||||
| tags | List/CSV of tags to create/update. | `true` | |
|
||||
| ref | The SHA or ref to tag. Defaults to SHA of current commit. | `false` | ${{ github.sha }} |
|
||||
| when_exists | What to do if the tag already exists. Must be one of 'update', 'skip', or 'fail'. | `false` | update |
|
||||
| parse_version | Version string to parse as SemVer and expose via handlebars in the tags input. | `false` | |
|
||||
| github_token | The GitHub token to use for authentication. | `false` | ${{ github.token }} |
|
||||
| parameter | description | required | default |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | ------------------- |
|
||||
| tags | List/CSV of tags to create/update. | `true` | |
|
||||
| ref | The SHA or ref to tag. Defaults to SHA of current commit. | `false` | ${{ github.sha }} |
|
||||
| parse | Version string to parse as SemVer and expose in the tags input via templating. | `false` | |
|
||||
| when_exists | What to do if the tag already exists. Must be one of 'update', 'skip', 'warn', or 'fail'. | `false` | update |
|
||||
| when_parse_fails | What to do with non-empty `parse` input that fails to parse as a Semantic Version. Must be one of 'warn' or 'fail'. | `false` | fail |
|
||||
| skip_prerelease | When `parse` input is pre-release version, should templated tags be skipped? | `false` | true |
|
||||
| github_token | The GitHub token to use for authentication. | `false` | ${{ github.token }} |
|
||||
|
||||
<!-- action-docs-inputs -->
|
||||
|
||||
|
||||
30
action.yml
30
action.yml
@@ -14,18 +14,30 @@ inputs:
|
||||
description: "The SHA or ref to tag. Defaults to SHA of current commit."
|
||||
required: false
|
||||
default: "${{ github.sha }}"
|
||||
when_exists:
|
||||
parse:
|
||||
description: >-
|
||||
What to do if the tag already exists. Must be one of 'update', 'skip', or
|
||||
'fail'.
|
||||
required: false
|
||||
default: "update"
|
||||
parse_version:
|
||||
description: >-
|
||||
Version string to parse as SemVer and expose via handlebars in the tags
|
||||
input.
|
||||
Version string to parse as SemVer and expose in the tags input via
|
||||
templating.
|
||||
required: false
|
||||
default: ""
|
||||
when_exists:
|
||||
description: >-
|
||||
What to do if the tag already exists. Must be one of 'update', 'skip',
|
||||
'warn', or 'fail'.
|
||||
required: false
|
||||
default: "update"
|
||||
when_parse_fails:
|
||||
description: >-
|
||||
What to do with non-empty `parse` input that fails to parse as a Semantic
|
||||
Version. Must be one of 'warn' or 'fail'.
|
||||
required: false
|
||||
default: "fail"
|
||||
skip_prerelease:
|
||||
description: >-
|
||||
When `parse` input is pre-release version, should templated tags be
|
||||
skipped?
|
||||
required: false
|
||||
default: "true"
|
||||
github_token:
|
||||
description: "The GitHub token to use for authentication."
|
||||
required: false
|
||||
|
||||
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
357
index.js
357
index.js
@@ -6,111 +6,56 @@ const handlebars = require("handlebars");
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
let parseVersion = core.getInput("parse_version");
|
||||
const defaultRef = core.getInput("ref");
|
||||
const token = core.getInput("github_token", { required: true });
|
||||
const whenExists = core.getInput("when_exists") || "update";
|
||||
|
||||
const tagsInput = core.getInput("tags", { required: true });
|
||||
const tagsRendered = parseVersionAndRenderTags(parseVersion, tagsInput);
|
||||
const defaultRef = core.getInput("ref");
|
||||
const inputVersion = core.getInput("parse");
|
||||
const whenExists = core.getInput("when_exists") || "update";
|
||||
const whenParseFails = core.getInput("when_parse_fails") || "fail";
|
||||
const skipPrerelease = core.getInput("skip_prerelease") === "true";
|
||||
const token = core.getInput("github_token", { required: true });
|
||||
|
||||
const parsedTags = csv
|
||||
.parse(tagsRendered, {
|
||||
delimiter: ",",
|
||||
trim: true,
|
||||
relax_column_count: true,
|
||||
})
|
||||
.flat();
|
||||
validateInput("when_exists", whenExists, [
|
||||
"update",
|
||||
"skip",
|
||||
"warn",
|
||||
"fail",
|
||||
]);
|
||||
validateInput("when_parse_fails", whenParseFails, ["warn", "fail"]);
|
||||
|
||||
const { owner, repo } = github.context.repo;
|
||||
|
||||
const uniqueRefs = new Set();
|
||||
const refToSha = {};
|
||||
const tags = {};
|
||||
|
||||
for (const tag of parsedTags) {
|
||||
const [t, tagRef] = tag.split(":").map((s) => s.trim());
|
||||
const ref = tagRef || defaultRef;
|
||||
tags[t] = ref;
|
||||
uniqueRefs.add(ref);
|
||||
}
|
||||
|
||||
const octokit = github.getOctokit(token);
|
||||
|
||||
// Pre-resolve all unique refs
|
||||
for (const ref of uniqueRefs) {
|
||||
refToSha[ref] = await resolveRefToSha(octokit, owner, repo, ref);
|
||||
}
|
||||
const tagsWithRefs = buildTags(
|
||||
core,
|
||||
tagsInput,
|
||||
defaultRef,
|
||||
inputVersion,
|
||||
whenParseFails,
|
||||
skipPrerelease
|
||||
);
|
||||
const tags = await resolveTags(core, octokit, owner, repo, tagsWithRefs);
|
||||
|
||||
const created = [];
|
||||
const updated = [];
|
||||
|
||||
// Create or update all tags by looping through tags
|
||||
// Create or update all tags
|
||||
for (const tagName in tags) {
|
||||
if (!tagName) {
|
||||
core.setFailed(`Invalid tag: '${tagName}'`);
|
||||
return;
|
||||
}
|
||||
const sha = tags[tagName];
|
||||
|
||||
const tagRef = tags[tagName];
|
||||
const sha = refToSha[tagRef];
|
||||
const res = await createOrUpdateTag(
|
||||
core,
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
tagName,
|
||||
sha,
|
||||
whenExists
|
||||
);
|
||||
|
||||
try {
|
||||
// Check if the ref exists
|
||||
const existing = await octokit.rest.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tagName}`,
|
||||
});
|
||||
|
||||
// If the ref exists, decide action based on 'when_exists'
|
||||
if (whenExists === "update") {
|
||||
const existingSHA = existing.data.object.sha;
|
||||
if (existingSHA === sha) {
|
||||
core.info(
|
||||
`Tag '${tagName}' already exists with desired SHA ${sha}.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Tag '${tagName}' exists, updating to SHA ${sha} ` +
|
||||
`(was ${existingSHA}).`
|
||||
);
|
||||
await octokit.rest.git.updateRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tagName}`,
|
||||
sha,
|
||||
force: true,
|
||||
});
|
||||
updated.push(tagName);
|
||||
} else if (whenExists === "skip") {
|
||||
core.info(`Tag '${tagName}' exists, skipping.`);
|
||||
} else if (whenExists === "fail") {
|
||||
core.setFailed(`Tag '${tagName}' already exists.`);
|
||||
return;
|
||||
} else {
|
||||
core.setFailed(
|
||||
`Invalid value for 'when_exists': '${whenExists}'. ` +
|
||||
`Valid values are 'update', 'skip', and 'fail'.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// If the ref doesn't exist, create it
|
||||
core.info(`Tag '${tagName}' does not exist, creating with SHA ${sha}.`);
|
||||
await octokit.rest.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/tags/${tagName}`,
|
||||
sha,
|
||||
});
|
||||
if (res === "created") {
|
||||
created.push(tagName);
|
||||
} else if (res === "updated") {
|
||||
updated.push(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,30 +67,208 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
function parseVersionAndRenderTags(parseVersion, tags) {
|
||||
if (!parseVersion) {
|
||||
return tags;
|
||||
function validateInput(name, value, allowedValues) {
|
||||
if (!allowedValues.includes(value)) {
|
||||
throw new Error(
|
||||
`Invalid value '${value}' for input '${name}'. ` +
|
||||
`Allowed values are: ${allowedValues.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (parseVersion.startsWith("refs/tags/")) {
|
||||
parseVersion = parseVersion.substring("refs/tags/".length);
|
||||
}
|
||||
|
||||
const version = semver.parse(parseVersion);
|
||||
|
||||
if (!version && tags.includes("{{")) {
|
||||
throw new Error(`Invalid version string: ${parseVersion}`);
|
||||
}
|
||||
|
||||
if (version) {
|
||||
const template = handlebars.compile(tags);
|
||||
tags = template(version);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
async function resolveRefToSha(octokit, owner, repo, ref) {
|
||||
function buildTags(
|
||||
core,
|
||||
tags,
|
||||
defaultRef,
|
||||
inputVersion,
|
||||
whenParseFail,
|
||||
skipPrerelease
|
||||
) {
|
||||
const list = csv
|
||||
.parse(tags, { delimiter: ",", trim: true, relax_column_count: true })
|
||||
.flat();
|
||||
|
||||
const parsedTags = {};
|
||||
const version = parseVersion(core, inputVersion, whenParseFail);
|
||||
|
||||
for (const item of list) {
|
||||
const [tag, ref] = item.split(":").map((s) => s.trim());
|
||||
const renderedTag = renderTag(core, tag, version, skipPrerelease);
|
||||
|
||||
if (renderedTag) {
|
||||
parsedTags[renderedTag] = ref || defaultRef;
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTags;
|
||||
}
|
||||
|
||||
function parseVersion(core, input, whenParseFail) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
const originalInput = input;
|
||||
|
||||
if (input.includes("/")) {
|
||||
input = input.split("/").pop();
|
||||
}
|
||||
|
||||
const version = semver.parse(input);
|
||||
if (version) {
|
||||
core.info(
|
||||
`Parsed input '${originalInput}' as semantic version: ${version.version}`
|
||||
);
|
||||
return version;
|
||||
}
|
||||
|
||||
if (whenParseFail === "fail") {
|
||||
throw new Error(`Failed to parse '${input}' as semantic version.`);
|
||||
}
|
||||
|
||||
core.warning(
|
||||
`Failed to parse '${input}'. Template-based tags will be skipped.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
function renderTag(core, tag, version, skipPrerelease) {
|
||||
if (!version) {
|
||||
if (!tag.includes("{{")) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
core.warning(
|
||||
`Skipping templated tag '${tag}'. No version information is available.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (version && version.includePrerelease && !skipPrerelease) {
|
||||
core.info(
|
||||
`Skipping templated tag '${tag}'. ` +
|
||||
`Parsed version '${version.version}' is a prerelease.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const template = handlebars.compile(tag);
|
||||
const emptyTag = template(semver.parse(""));
|
||||
const renderedTag = template(version);
|
||||
|
||||
if (emptyTag === renderedTag) {
|
||||
core.info(
|
||||
`Skipping templated tag '${tag}', all used template variables are empty.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (renderedTag.includes("{{")) {
|
||||
throw new Error(
|
||||
`Templated tag '${tag}' could not be renderd, some template ` +
|
||||
`variables could be resolved. Rendered to '${renderedTag}'.`
|
||||
);
|
||||
}
|
||||
|
||||
return renderedTag;
|
||||
}
|
||||
|
||||
async function resolveTags(core, octokit, owner, repo, tags) {
|
||||
const uniqueRefs = new Set();
|
||||
for (const tagName in tags) {
|
||||
uniqueRefs.add(tags[tagName]);
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Looking up commit details for: '${Array.from(uniqueRefs).join("', '")}'`
|
||||
);
|
||||
const refToSha = {};
|
||||
for (const ref of uniqueRefs) {
|
||||
const sha = await resolveRefToSha(core, octokit, owner, repo, ref);
|
||||
if (sha) {
|
||||
refToSha[ref] = sha;
|
||||
}
|
||||
}
|
||||
|
||||
const tagShas = {};
|
||||
for (const tagName in tags) {
|
||||
const ref = tags[tagName];
|
||||
const sha = refToSha[ref];
|
||||
|
||||
if (!sha) {
|
||||
core.warning(
|
||||
`Skipping tag '${tagName}'. No commit details found for '${ref}'.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tagShas[tagName] = sha;
|
||||
}
|
||||
|
||||
return tagShas;
|
||||
}
|
||||
|
||||
async function createOrUpdateTag(
|
||||
core,
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
tagName,
|
||||
sha,
|
||||
whenExists
|
||||
) {
|
||||
try {
|
||||
const existing = await octokit.rest.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tagName}`,
|
||||
});
|
||||
|
||||
if (whenExists === "update") {
|
||||
const existingSHA = existing.data.object.sha;
|
||||
if (existingSHA === sha) {
|
||||
core.info(`Tag '${tagName}' already exists with desired SHA ${sha}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Tag '${tagName}' exists, updating to SHA ${sha} ` +
|
||||
`(was ${existingSHA}).`
|
||||
);
|
||||
await octokit.rest.git.updateRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tagName}`,
|
||||
sha,
|
||||
force: true,
|
||||
});
|
||||
|
||||
return "updated";
|
||||
} else if (whenExists === "skip") {
|
||||
core.info(`Tag '${tagName}' exists, skipping.`);
|
||||
return "skipped";
|
||||
} else if (whenExists === "warn") {
|
||||
core.warning(`Tag '${tagName}' exists, skipping.`);
|
||||
return "skipped";
|
||||
} else {
|
||||
throw new Error(`Tag '${tagName}' already exists.`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info(`Tag '${tagName}' does not exist, creating with SHA ${sha}.`);
|
||||
await octokit.rest.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/tags/${tagName}`,
|
||||
sha,
|
||||
});
|
||||
|
||||
return "created";
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveRefToSha(core, octokit, owner, repo, ref) {
|
||||
try {
|
||||
const {
|
||||
data: { sha },
|
||||
@@ -153,16 +276,20 @@ async function resolveRefToSha(octokit, owner, repo, ref) {
|
||||
|
||||
return sha;
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to resolve ref '${ref}' to a SHA: ${error}`;
|
||||
throw new Error(errorMessage);
|
||||
core.warning(`Failed to fetch commit details for '${ref}'.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Export run function for testing
|
||||
module.exports = {
|
||||
run,
|
||||
parseVersion,
|
||||
buildTags,
|
||||
renderTag,
|
||||
resolveTags,
|
||||
createOrUpdateTag,
|
||||
resolveRefToSha,
|
||||
parseVersionAndRenderTags,
|
||||
};
|
||||
|
||||
// Call run function to start action only if this file is being run directly.
|
||||
|
||||
Reference in New Issue
Block a user