Updated to use new strategies with controlled Docker environment.

This commit is contained in:
Corey Butler 2020-04-01 12:27:19 -05:00
parent 3071dd056e
commit d1f68225dd
15 changed files with 447 additions and 241 deletions

30
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Test
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checkout updated source code
- uses: actions/checkout@v2
# If the version has changed, create a new git tag for it.
- name: Tag
id: autotagger
uses: butlerlogic/action-autotag@master
with:
tag_prefix: 'test_'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Rollback Release
if: failure() && steps.create_release.outputs.id != ''
uses: author/action-rollback@stable
with:
tag: ${{ steps.autotagger.outputs.tagname }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

13
.gitignore vendored
View File

@ -1,16 +1,9 @@
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
*.log*
.*
!.gitignore
!.github
!.dockerignore
_*
node_modules

5
Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM node:13-alpine
ADD ./app /app
WORKDIR /app
RUN npm i
CMD ["node", "main.js"]

View File

@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2019 Corey Butler and contributors
Copyright (c) 2020 Corey Butler and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,8 +1,14 @@
# Autotag
This action will read a `package.json` file and compare the `version` attribute to the project's known tags. If a corresponding tag does not exist, it will be created.
This action will auto-generate a Github tag whenever a new version is detected. The following "detection strategies" are available:
This tag works well in combination with:
1. **package**: Monitor a `package.json` for new versions.
1. **docker**: Monitor a `Dockerfile` for a `LABEL version=x.x.x` value.
1. **regex**: Use a JavaScript [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) with any file for your own custom extraction.
When a version is detected, it is compared to the current list of tags in the Github repository. If a tag does not exist, it will be created.
This action works well in combination with:
- [actions/create-release](https://github.com/actions/create-release) (Auto-release)
- [author/action-publish](https://github.com/author/action-publish) (Auto-publish JavaScript/Node modules)
@ -44,7 +50,7 @@ This **order** is important!
## Configuration
The `GITHUB_TOKEN` must be passed in. Without this, it is not possible to create a new tag. Make sure the autotag action looks like the following example:
The `GITHUB_TOKEN` **must** be provided. Without this, it is not possible to create a new tag. Make sure the autotag action looks like the following example:
```yaml
- uses: butlerlogic/action-autotag@stable
@ -58,7 +64,18 @@ The action will automatically extract the token at runtime. **DO NOT MANUALLY EN
There are several options to customize how the tag is created.
1. `package_root`
#### strategy
This is the strategy used to identify the version number/tag from within the code base.
1. _package_: Monitor a `package.json` for new versions. Use this for JavaScript projects based on Node modules (npm, yarn, etc).
1. _docker_: Monitor a `Dockerfile` for a `LABEL version=x.x.x` value. USe this for container projects.
1. _regex*_: Use a JavaScript [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) with any file for your own custom extraction.
*An example "
#### root `(required)`
_Formerly `package_root`_
By default, autotag will look for the `package.json` file in the project root. If the file is located in a subdirectory, this option can be used to point to the correct file.
@ -66,12 +83,14 @@ There are several options to customize how the tag is created.
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
package_root: "/path/to/subdirectory"
root: "/path/to/subdirectory"
```
1. `tag_prefix`
> **EXCEPTION**: This property is not required if the regex_pattern property is defined. In that case, this property is assumed to be "regex".
By default, `package.json` uses [semantic versioning](https://semver.org/), such as `1.0.0`. A prefix can be used to add text before the tag name. For example, if `tag_prefix` is set to `v`, then the tag would be labeled as `v1.0.0`.
#### tag_prefix
By default, [semantic versioning](https://semver.org/) is used, such as `1.0.0`. A prefix can be used to add text before the tag name. For example, if `tag_prefix` is set to `v`, then the tag would be labeled as `v1.0.0`.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
@ -80,9 +99,9 @@ There are several options to customize how the tag is created.
tag_prefix: "v"
```
1. `tag_suffix`
#### tag_suffix
Text can also be applied to the end of the tag by setting `tag_suffix`. For example, if `tag_suffix` is ` (beta)`, the tag would be `1.0.0 (beta)`. Please note this example violates semantic versioning and is merely here to illustrate how to add text to the end of a tag name if you _really_ want to.
Text can be applied to the end of the tag by setting `tag_suffix`. For example, if `tag_suffix` is ` (beta)`, the tag would be `1.0.0 (beta)`. Please note this example violates semantic versioning and is merely here to illustrate how to add text to the end of a tag name if you _really_ want to.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
@ -91,10 +110,9 @@ There are several options to customize how the tag is created.
tag_suffix: " (beta)"
```
1. `tag_message`
#### tag_message
This is the annotated commit message associated with the tag. By default, a
changelog will be generated from the commits between the latest tag and the new tag (HEAD). Setting this option will override it witha custom message.
This is the annotated commit message associated with the tag. By default, a changelog will be generated from the commits between the latest tag and the current reference (HEAD). Setting this option will override the message.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
@ -103,10 +121,11 @@ There are several options to customize how the tag is created.
tag_message: "Custom message goes here."
```
1. `version`
#### version
Explicitly set the version instead of automatically detecting from `package.json`.
Useful for non-JavaScript projects where version may be output by a previous action.
Explicitly set the version instead of using automatic detection.
Useful for projects where the version number may be output by a previous action.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
@ -115,6 +134,19 @@ There are several options to customize how the tag is created.
version: "${{ steps.previous_step.outputs.version }}"
```
#### regex_pattern
An optional attribute containing the regular expression used to extract the version number.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
regex_pattern: "version=([0-9\.]+)"
```
This attribute is used as the first argument of a [RegExp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp) object. The first "group" (i.e. what's in parenthesis) will be used as the version number. For an example, see this [working example](regexr.com/51i6n).
## Developer Notes
If you are building an action that runs after this one, be aware this action produces several [outputs](https://help.github.com/en/articles/metadata-syntax-for-github-actions#outputs):
@ -123,6 +155,7 @@ If you are building an action that runs after this one, be aware this action pro
1. `tagsha`: The SHA of the new tag.
1. `taguri`: The URI/URL of the new tag reference.
1. `tagmessage`: The messge applied to the tag reference (this is what shows up on the tag screen on Github).
1. `tagcreated`: `yes` or `no`.
1. `version` will be the version attribute found in the `package.json` file.
---
@ -135,10 +168,10 @@ This action was written and is primarily maintained by [Corey Butler](https://gi
If you use this or find value in it, please consider contributing in one or more of the following ways:
1. Click the "Sponsor" button at the top of the page.
1. Click the "Sponsor" button at the top of the page and make a contribution.
1. Star it!
1. [Tweet about it!](https://twitter.com/intent/tweet?hashtags=github,actions&original_referer=http%3A%2F%2F127.0.0.1%3A91%2F&text=I%20am%20automating%20my%20workflow%20with%20the%20Autotagger%20Github%20action!&tw_p=tweetbutton&url=https%3A%2F%2Fgithub.com%2Fmarketplace%2Factions%2Fautotagger&via=goldglovecb)
1. Fix an issue.
1. Add a feature (post a proposal in an issue first!).
Copyright © 2019 ButlerLogic, Corey Butler, and Contributors.
Copyright © 2020 Butler Logic, Corey Butler, and Contributors.

View File

@ -1,12 +1,20 @@
name: "Autotagger"
description: "Automatically generate new tags when the package.json version changes."
description: "Automatically generate new tags for new versions. Supports several tagging strategies, including package.json, Dockerfiles, and Regex."
author: "Butler Logic"
branding:
icon: "tag"
color: "black"
color: "blue"
inputs:
root:
description: Autotag will look for the appropriate file in in this location (relative to project root).
required: false
default: './'
strategy:
description: Options include 'package' (for package.json), 'docker' (for Dockerfile), and 'regex' to extract from an arbitrary file. This does not need to be specified if the "regex_pattern" property is provided.
required: false
default: 'package'
package_root:
description: Autotag will look for the package.json file in in this location.
description: (DEPRECATED. Use 'root' instead.) Autotag will look for the package.json file in in this location.
required: false
default: './'
tag_prefix:
@ -32,6 +40,8 @@ outputs:
description: The messge applied to the tag reference (this is what shows up on the tag screen on Github).
version:
description: The version, as defined in package.json or explicitly set in the input.
tagcreated:
description: A "yes" or "no", indicating a new tag was created.
runs:
using: "node12"
main: "lib/main.js"
using: 'docker'
image: 'Dockerfile'

15
app/lib/docker.js Normal file
View File

@ -0,0 +1,15 @@
import Regex from './regex.js'
import path from 'path'
import fs from 'fs'
export default class Dockerfile extends Regex {
constructor (root = null) {
root = path.join(process.env.GITHUB_WORKSPACE, root)
if (fs.statSync(root).isDirectory()) {
root = path.join(root, 'Dockerfile')
}
super(root, /LABEL[\s\t]+version=[\t\s+]?[\"\']?([0-9\.]+)[\"\']?/i)
}
}

23
app/lib/package.js Normal file
View File

@ -0,0 +1,23 @@
import fs from 'fs'
import path from 'path'
export default class Package {
constructor (root = './') {
root = path.join(process.env.GITHUB_WORKSPACE, root)
if (fs.statSync(root).isDirectory()) {
root = path.join(root, 'package.json')
}
if (!fs.existsSync(root)) {
throw new Error(`package.json does not exist at ${root}.`)
}
this.root = root
this.data = JSON.parse(fs.readFileSync(root))
}
get version () {
return this.data.version
}
}

34
app/lib/regex.js Normal file
View File

@ -0,0 +1,34 @@
import fs from 'fs'
import path from 'path'
export default class Regex {
constructor (root = null, pattern) {
root = path.join(process.env.GITHUB_WORKSPACE, root)
if (fs.statSync(root).isDirectory()) {
throw new Error(`${root} is a directory. The Regex tag identification strategy requires a file.`)
}
if (!fs.existsSync(root)) {
throw new Error(`"${root}" does not exist.`)
}
this.content = fs.readFileSync(root).toString()
let content = pattern.exec(this.content)
if (!content) {
this._version = null
// throw new Error(`Could not find pattern matching "${pattern.toString()}" in "${root}".`)
} else {
this._version = content[1]
}
}
get version () {
return this._version
}
get versionFound () {
return this._version !== null
}
}

33
app/lib/setup.js Normal file
View File

@ -0,0 +1,33 @@
import core from '@actions/core'
import fs from 'fs'
import path from 'path'
export default class Setup {
static debug () {
// Metadate for debugging
core.debug(
` Available environment variables:\n -> ${Object.keys(process.env)
.map(i => i + ' :: ' + process.env[i])
.join('\n -> ')}`
)
const dir = fs
.readdirSync(path.resolve(process.env.GITHUB_WORKSPACE), { withFileTypes: true })
.map(entry => {
return `${entry.isDirectory() ? '> ' : ' - '}${entry.name}`
})
.join('\n')
core.debug(` Working Directory: ${process.env.GITHUB_WORKSPACE}:\n${dir}`)
}
static requireAnyEnv () {
for (const arg of arguments) {
if (!process.env.hasOwnProperty(arg)) {
return
}
}
throw new Error('At least one of the following environment variables is required: ' + Array.slice(arguments).join(', '))
}
}

135
app/lib/tag.js Normal file
View File

@ -0,0 +1,135 @@
import core from '@actions/core'
import os from 'os'
import { GitHub, context } from '@actions/github'
// Get authenticated GitHub client (Ocktokit): https://github.com/actions/toolkit/tree/master/packages/github#usage
const github = new GitHub(process.env.GITHUB_TOKEN || process.env.INPUT_GITHUB_TOKEN)
// Get owner and repo from context of payload that triggered the action
const { owner, repo } = context.repo
export default class Tag {
constructor (prefix, version, postfix) {
this.prefix = prefix
this.version = version
this.postfix = postfix
this._tags = null
this._message = null
this._exists = null
}
get name () {
return `${this.prefix.trim()}${this.version.trim()}${this.postfix.trim()}`
}
set message (value) {
if (value && value.length > 0) {
this._message = value
}
}
async getMessage () {
if (this._message !== null) {
return this._message
}
try {
const changelog = await github.repos.compareCommits({ owner, repo, base: tags.data.shift().name, head: 'master' })
return changelog.data.commits
.map(
(commit, i) =>
`${i + 1}) ${commit.commit.message}${
commit.hasOwnProperty('author')
? commit.author.hasOwnProperty('login')
? ' (' + commit.author.login + ')'
: ''
: ''
}\n(SHA: ${commit.sha})\n`
)
.join('\n')
} catch (e) {
core.warning('Failed to generate changelog from commits: ' + e.message + os.EOL)
return `Version ${this.version}`
}
}
async getTags () {
if (this._tags !== null) {
return this._tags.data
}
this._tags = await github.repos.listTags({ owner, repo, per_page: 100 })
return this._tags.data
}
async exists () {
if (this._exists !== null) {
return this._exists
}
const currentTag = this.name
const tags = await this.getTags()
for (const tag of tags) {
if (tag.name === currentTag) {
this._exists = true
return true
}
}
this._exists = false
return false
}
async push () {
let tagexists = await this.exists()
if (!tagexists) {
// Create tag
const newTag = await github.git.createTag({
owner,
repo,
tag: this.name,
message: this.message,
object: process.env.GITHUB_SHA,
type: 'commit'
})
core.warning(`Created new tag: ${newTag.data.tag}`)
// Create reference
let newReference
try {
newReference = await github.git.createRef({
owner,
repo,
ref: `refs/tags/${newTag.data.tag}`,
sha: newTag.data.sha
})
} catch (e) {
core.warning({
owner,
repo,
ref: `refs/tags/${newTag.data.tag}`,
sha: newTag.data.sha
})
throw e
}
core.warning(`Reference ${newReference.data.ref} available at ${newReference.data.url}` + os.EOL)
// Store values for other actions
if (typeof newTag === 'object' && typeof newReference === 'object') {
core.setOutput('tagname', this.name)
core.setOutput('tagsha', newTag.data.sha)
core.setOutput('taguri', newReference.data.url)
core.setOutput('tagmessage', this.message)
core.setOutput('tagref', newReference.data.ref)
core.setOutput('tagcreated', 'yes')
}
} else {
core.warning('Cannot push tag (it already exists).')
}
}
}

72
app/main.js Normal file
View File

@ -0,0 +1,72 @@
import core from '@actions/core'
import os from 'os'
import Setup from './lib/setup.js'
import Package from './lib/package.js'
import Tag from './lib/tag.js'
import Regex from './lib/regex.js'
async function run () {
try {
Setup.debug()
Setup.requireAnyEnv('GITHUB_TOKEN', 'INPUT_GITHUB_TOKEN')
// Identify the tag parsing strategy
const root = core.getInput('root', { required: false }) || core.getInput('package_root', { required: false }) || './'
const strategy = (core.getInput('strategy', { required: false }) || '').trim().length > 0 ? 'regex' : ((core.getInput('strategy', { required: false }) || 'package').trim().toLowerCase())
// Extract the version number using the supplied strategy
let version = core.getInput('root', { required: false })
version = version === null || version.trim().length === 0 ? null : version
switch (strategy) {
case 'docker':
version = (new Dockerfile(root)).version
break
case 'package':
// Extract using the package strategy (this is the default strategy)
version = (new Package(root)).version
break
case 'regex':
version = (new Regex(root, new RegExp(pattern, 'i'))).version
break
default:
core.setFailed(`"${strategy}" is not a recognized tagging strategy. Choose from: 'package' (package.json), 'docker' (uses Dockerfile), or 'regex' (JS-based RegExp).`)
return
}
core.setOutput('version', version)
core.debug(` Detected version ${version}`)
// Configure a tag using the identified version
const tag = new Tag(
core.getInput('tag_prefix', { required: false }),
version,
core.getInput('tag_suffix', { required: false })
)
// Check for existance of tag and abort (short circuit) if it already exists.
if (await tag.exists()) {
core.warning(`"${tag.name}" tag already exists.` + os.EOL)
core.setOutput('tagname', '')
core.setOutput('tagcreated', 'no')
return
}
// The tag setter will autocorrect the message if necessary.
tag.message = core.getInput('tag_message', { required: false }).trim()
await tag.push()
} catch (error) {
core.warning(error.message)
core.setOutput('tagname', '')
core.setOutput('tagsha', '')
core.setOutput('taguri', '')
core.setOutput('tagmessage', '')
core.setOutput('tagref', '')
core.setOutput('tagcreated', 'no')
}
}
run()

View File

@ -4,9 +4,6 @@
"private": true,
"description": "Automatically create a tag whenever the version changes in package.json",
"main": "lib/main.js",
"scripts": {
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/butlerlogic/action-autotag.git"

View File

@ -1,174 +0,0 @@
const core = require('@actions/core')
const { GitHub, context } = require('@actions/github')
const fs = require('fs')
const path = require('path')
const os = require('os')
async function run() {
try {
core.debug(
` Available environment variables:\n -> ${Object.keys(process.env)
.map(i => i + ' :: ' + process.env[i])
.join('\n -> ')}`
)
const dir = fs
.readdirSync(path.resolve(process.env.GITHUB_WORKSPACE), { withFileTypes: true })
.map(entry => {
return `${entry.isDirectory() ? '> ' : ' - '}${entry.name}`
})
.join('\n')
core.debug(` Working Directory: ${process.env.GITHUB_WORKSPACE}:\n${dir}`)
if (!process.env.hasOwnProperty('GITHUB_TOKEN')) {
if (!process.env.hasOwnProperty('INPUT_GITHUB_TOKEN')) {
core.setFailed('Invalid or missing GITHUB_TOKEN.')
return
}
}
const pkg_root = core.getInput('package_root', { required: false })
const pkgfile = path.join(process.env.GITHUB_WORKSPACE, pkg_root, 'package.json')
if (!fs.existsSync(pkgfile)) {
core.setFailed('package.json does not exist.')
return
}
const pkg = require(pkgfile)
core.setOutput('version', pkg.version)
core.debug(` Detected version ${pkg.version}`)
// Get authenticated GitHub client (Ocktokit): https://github.com/actions/toolkit/tree/master/packages/github#usage
const github = new GitHub(process.env.GITHUB_TOKEN || process.env.INPUT_GITHUB_TOKEN)
// Get owner and repo from context of payload that triggered the action
const { owner, repo } = context.repo
// // Check for existing tag
// const git = new github.GitHub(process.env.INPUT_GITHUB_TOKEN || process.env.GITHUB_TOKEN)
// const owner = process.env.GITHUB_REPOSITORY.split('/').shift()
// const repo = process.env.GITHUB_REPOSITORY.split('/').pop()
let tags
try {
tags = await github.repos.listTags({
owner,
repo,
per_page: 100,
})
} catch (e) {
tags = {
data: [],
}
}
const tagPrefix = core.getInput('tag_prefix', { required: false })
const tagSuffix = core.getInput('tag_suffix', { required: false })
const getTagName = version => {
return `${tagPrefix}${version}${tagSuffix}`
}
// Check for existance of tag and abort (short circuit) if it already exists.
for (let tag of tags.data) {
if (tag.name === getTagName(pkg.version)) {
core.warning(`"${tag.name.trim()}" tag already exists.` + os.EOL)
core.setOutput('tagname', '')
return
}
}
// Create the new tag name
const tagName = getTagName(pkg.version)
let tagMsg = core.getInput('tag_message', { required: false }).trim()
if (tagMsg.length === 0 && tags.data.length > 0) {
try {
latestTag = tags.data.shift()
let changelog = await github.repos.compareCommits({
owner,
repo,
base: latestTag.name,
head: 'master',
})
tagMsg = changelog.data.commits
.map(
commit =>
`**1) ${commit.commit.message}**${
commit.hasOwnProperty('author')
? commit.author.hasOwnProperty('login')
? ' (' + commit.author.login + ')'
: ''
: ''
}\n(SHA: ${commit.sha})\n`
)
.join('\n')
} catch (e) {
core.warning('Failed to generate changelog from commits: ' + e.message + os.EOL)
tagMsg = tagName
}
}
let newTag
try {
tagMsg = tagMsg.trim().length > 0 ? tagMsg : `Version ${pkg.version}`
newTag = await github.git.createTag({
owner,
repo,
tag: tagName,
message: tagMsg,
object: process.env.GITHUB_SHA,
type: 'commit'
})
core.warning(`Created new tag: ${newTag.data.tag}`)
} catch (e) {
core.setFailed(e.message)
return
}
let newReference
try {
newReference = await github.git.createRef({
owner,
repo,
ref: `refs/tags/${newTag.data.tag}`,
sha: newTag.data.sha,
})
core.warning(`Reference ${newReference.data.ref} available at ${newReference.data.url}` + os.EOL)
} catch (e) {
core.warning({
owner,
repo,
ref: `refs/tags/${newTag.data.tag}`,
sha: newTag.data.sha,
})
core.setFailed(e.message)
return
}
// Store values for other actions
if (typeof newTag === 'object' && typeof newReference === 'object') {
core.setOutput('tagname', tagName)
core.setOutput('tagsha', newTag.data.sha)
core.setOutput('taguri', newReference.data.url)
core.setOutput('tagmessage', tagMsg.trim())
core.setOutput('tagref', newReference.data.ref)
}
} catch (error) {
core.warning(error.message)
core.setOutput('tagname', '')
core.setOutput('tagsha', '')
core.setOutput('taguri', '')
core.setOutput('tagmessage', '')
core.setOutput('tagref', '')
}
}
run()