mirror of
https://github.com/jimeh/update-tags-action.git
synced 2026-02-19 01:26:40 +00:00
299 lines
6.8 KiB
JavaScript
299 lines
6.8 KiB
JavaScript
const core = require("@actions/core");
|
|
const github = require("@actions/github");
|
|
const csv = require("csv-parse/sync");
|
|
const semver = require("semver");
|
|
const handlebars = require("handlebars");
|
|
|
|
async function run() {
|
|
try {
|
|
const tagsInput = core.getInput("tags", { required: true });
|
|
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 });
|
|
|
|
validateInput("when_exists", whenExists, [
|
|
"update",
|
|
"skip",
|
|
"warn",
|
|
"fail",
|
|
]);
|
|
validateInput("when_parse_fails", whenParseFails, ["warn", "fail"]);
|
|
|
|
const { owner, repo } = github.context.repo;
|
|
const octokit = github.getOctokit(token);
|
|
|
|
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
|
|
for (const tagName in tags) {
|
|
const sha = tags[tagName];
|
|
|
|
const res = await createOrUpdateTag(
|
|
core,
|
|
octokit,
|
|
owner,
|
|
repo,
|
|
tagName,
|
|
sha,
|
|
whenExists
|
|
);
|
|
|
|
if (res === "created") {
|
|
created.push(tagName);
|
|
} else if (res === "updated") {
|
|
updated.push(tagName);
|
|
}
|
|
}
|
|
|
|
core.setOutput("created", created);
|
|
core.setOutput("updated", updated);
|
|
core.setOutput("tags", created.concat(updated));
|
|
} catch (error) {
|
|
core.setFailed(`Action failed with error: ${error}`);
|
|
}
|
|
}
|
|
|
|
function validateInput(name, value, allowedValues) {
|
|
if (!allowedValues.includes(value)) {
|
|
throw new Error(
|
|
`Invalid value '${value}' for input '${name}'. ` +
|
|
`Allowed values are: ${allowedValues.join(", ")}`
|
|
);
|
|
}
|
|
}
|
|
|
|
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 },
|
|
} = await octokit.rest.repos.getCommit({ owner, repo, ref });
|
|
|
|
return sha;
|
|
} catch (error) {
|
|
core.warning(`Failed to fetch commit details for '${ref}'.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Export run function for testing
|
|
module.exports = {
|
|
run,
|
|
parseVersion,
|
|
buildTags,
|
|
renderTag,
|
|
resolveTags,
|
|
createOrUpdateTag,
|
|
resolveRefToSha,
|
|
};
|
|
|
|
// Call run function to start action only if this file is being run directly.
|
|
if (require.main === module) {
|
|
run();
|
|
}
|