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

127
README.md
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,62 +64,88 @@ 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
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.
This is the strategy used to identify the version number/tag from within the code base.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
package_root: "/path/to/subdirectory"
```
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.
1. `tag_prefix`
*An example "
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`.
#### root `(required)`
_Formerly `package_root`_
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
```
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.
1. `tag_suffix`
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
root: "/path/to/subdirectory"
```
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.
> **EXCEPTION**: This property is not required if the regex_pattern property is defined. In that case, this property is assumed to be "regex".
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_suffix: " (beta)"
```
#### tag_prefix
1. `tag_message`
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`.
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.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
```
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_message: "Custom message goes here."
```
#### tag_suffix
1. `version`
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.
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.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_suffix: " (beta)"
```
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
version: "${{ steps.previous_step.outputs.version }}"
```
#### 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 current reference (HEAD). Setting this option will override the message.
```yaml
- uses: butlerlogic/action-autotag@1.0.0
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_message: "Custom message goes here."
```
#### version
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
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
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
@ -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."
author: "ButlerLogic"
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()