wip: further support for parsing input version and templated tags

This commit is contained in:
2023-05-24 00:41:47 +01:00
parent 77c38f4b74
commit ef25cc7b7d
4 changed files with 273 additions and 132 deletions

View File

@@ -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 -->

View File

@@ -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

File diff suppressed because one or more lines are too long

357
index.js
View File

@@ -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.