├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── action.yml └── main.js /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # Test the action itself 3 | ## 4 | name: Test 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | - feature/** 11 | workflow_dispatch: 12 | 13 | jobs: 14 | # test action on Ubuntu 15 | test-ubuntu: 16 | runs-on: ubuntu-latest 17 | permissions: write-all 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Test generating build number on Ubuntu 21 | uses: ./ 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | prefix: test 25 | delete_previous_tag: false 26 | - run: | 27 | if [[ -z "${BUILD_NUMBER}" ]]; then 28 | echo >&2 "BUILD_NUMBER environment variable is undefined!" 29 | exit 1 30 | fi 31 | 32 | # test action on Ubuntu 33 | test-windows: 34 | runs-on: windows-latest 35 | permissions: write-all 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Test generating build number on Windows 39 | uses: ./ 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | prefix: test 43 | delete_previous_tag: false 44 | - run: | 45 | if (-not (Test-Path env:BUILD_NUMBER)) { 46 | Write-Error "BUILD_NUMBER environment variable is undefined!" 47 | exit 1 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Onyx Mueller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Tag Number Action 2 | 3 | [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Build%20Tag%20Number-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](https://github.com/onyxmueller/build-tag-number) 4 | 5 | GitHub action for generating sequential build numbers based on Git tag. The build number is stored in your GitHub repository as a ref, it doesn't add any extra commits to your repository. Use in your workflow like so: 6 | 7 | ```yaml 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Generate build number 13 | uses: onyxmueller/build-tag-number@v1 14 | with: 15 | token: ${{secrets.github_token}} 16 | - name: Print new build number 17 | run: echo "Build number is $BUILD_NUMBER" 18 | # Or, if you're on Windows: echo "Build number is ${env:BUILD_NUMBER}" 19 | ``` 20 | 21 | After that runs the subsequent steps in your job will have the environment variable `BUILD_NUMBER` available. If you prefer to be more explicit you can use the output of the step, like so: 22 | 23 | ```yaml 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Generate build number 29 | id: buildnumber 30 | uses: onyxmueller/build-tag-number@v1 31 | with: 32 | token: ${{secrets.github_token}} 33 | 34 | # Now you can pass ${{ steps.buildnumber.outputs.build_number }} to the next steps. 35 | - name: Another step as an example 36 | uses: actions/hello-world-docker-action@v1 37 | with: 38 | who-to-greet: ${{ steps.buildnumber.outputs.build_number }} 39 | ``` 40 | The `GITHUB_TOKEN` environment variable is defined by GitHub for you. See [virtual environments for GitHub actions](https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret) for more information. 41 | 42 | ## Getting the build number in other jobs 43 | 44 | For other steps in the same job, you can use the methods above, 45 | to get the build number in other jobs you need to use [job outputs](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjobs_idoutputs) mechanism: 46 | 47 | ```yaml 48 | jobs: 49 | job1: 50 | runs-on: ubuntu-latest 51 | outputs: 52 | build_number: ${{ steps.buildnumber.outputs.build_number }} 53 | steps: 54 | - name: Generate build number 55 | id: buildnumber 56 | uses: onyxmueller/build-tag-number@v1 57 | with: 58 | token: ${{secrets.github_token}} 59 | 60 | job2: 61 | needs: job1 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Another step as an example 65 | uses: actions/hello-world-docker-action@v1 66 | with: 67 | who-to-greet: ${{needs.job1.outputs.build_number}} 68 | ``` 69 | 70 | ## Setting the initial build number. 71 | 72 | If you're moving from another build system, you might want to start from some specific number. The `build-number` action simply uses a special tag name to store the build number, `build-number-x`, so you can just create and push a tag with the number you want to start on. E.g. do 73 | 74 | ``` 75 | git tag build-number-500 76 | git push origin build-number-500 77 | ``` 78 | 79 | and then your next build number will be 501. The action will always delete older refs that start with `build-number-`, e.g. when it runs and finds `build-number-500` it will create a new tag, `build-number-501` and then delete `build-number-500`. 80 | 81 | ## Generating multiple independent build numbers 82 | 83 | Sometimes you may have more than one project to build in one repository. For example, you may have a client and a server in the same GitHub repository that you would like to generate independent build numbers for. Another example is you have two Dockerfiles in one repo and you'd like to version each of the built images with their own numbers. 84 | To do this, use the `prefix` key, like so: 85 | 86 | ```yaml 87 | jobs: 88 | build: 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Generate build number 92 | id: buildnumber 93 | uses: onyxmueller/build-tag-number@v1 94 | with: 95 | token: ${{ secrets.github_token }} 96 | prefix: client 97 | ``` 98 | 99 | This will generate a git tag like `client-build-number-1`. 100 | 101 | If you then do the same in another workflow and use `prefix: server` then you'll get a second build-number tag called `server-build-number-1`. 102 | 103 | ### Optional: Do not delete previous tags 104 | By default, the action deletes the previous build-number tag after creating a new one. If you want to keep all previous build-number tags, set `delete_previous_tag: false`: 105 | 106 | ```yaml 107 | jobs: 108 | build: 109 | runs-on: ubuntu-latest 110 | steps: 111 | - name: Generate build number 112 | uses: onyxmueller/build-tag-number@v1 113 | with: 114 | token: ${{secrets.github_token}} 115 | delete_previous_tag: false 116 | ``` 117 | 118 | ## Branches and build numbers 119 | 120 | The build number generator is global, there's no concept of special build numbers for special branches unless handled manually with the `prefix` property. It's probably something you would just use on builds from your master branch. It's just one number that gets increased every time the action is run. 121 | 122 | ## Find this library useful? :raised_hands: 123 | Support it by joining __[stargazers](https://github.com/onyxmueller/build-tag-number/stargazers)__ for this repository. :star: And __[follow me](https://github.com/onyxmueller)__ for other creations. 124 | 125 | ## Credit 126 | 127 | This Github Action is based on original work done by Einar Egilsson, which is no longer maintained. You can read more about the original version on his blog: 128 | 129 | http://einaregilsson.com/a-github-action-for-generating-sequential-build-numbers/ 130 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build Tag Number' 2 | description: 'Sequential build numbers for workflow runs based on Git tag' 3 | author: 'Onyx Mueller' 4 | runs: 5 | using: 'node20' 6 | main: 'main.js' 7 | inputs: 8 | token: 9 | description: 'GitHub Token to create and delete refs (GITHUB_TOKEN)' 10 | required: false # Not required when getting the stored build number for later jobs, only in the first jobs when it's generated 11 | prefix: 12 | description: 'Prefix for the build-number- tag to make it unique if tracking multiple build numbers' 13 | required: false 14 | delete_previous_tag: 15 | description: 'If set to false, previous build number tags will not be deleted (default: true)' 16 | required: false 17 | 18 | outputs: 19 | build_number: 20 | description: 'Build tag number' 21 | 22 | branding: 23 | icon: 'hash' 24 | color: 'green' 25 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | //Trying to avoid any npm installs or anything that takes extra time... 2 | const https = require('https'), 3 | zlib = require('zlib'), 4 | fs = require('fs'), 5 | env = process.env; 6 | 7 | function fail(message, exitCode=1) { 8 | console.log(`::error::${message}`); 9 | process.exit(1); 10 | } 11 | 12 | function request(method, path, data, callback) { 13 | 14 | try { 15 | if (data) { 16 | data = JSON.stringify(data); 17 | } 18 | const options = { 19 | hostname: 'api.github.com', 20 | port: 443, 21 | path, 22 | method, 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | 'Content-Length': data ? data.length : 0, 26 | 'Accept-Encoding' : 'gzip', 27 | 'Authorization' : `token ${env.INPUT_TOKEN}`, 28 | 'User-Agent' : 'GitHub Action - development' 29 | } 30 | } 31 | const req = https.request(options, res => { 32 | 33 | let chunks = []; 34 | res.on('data', d => chunks.push(d)); 35 | res.on('end', () => { 36 | let buffer = Buffer.concat(chunks); 37 | if (res.headers['content-encoding'] === 'gzip') { 38 | zlib.gunzip(buffer, (err, decoded) => { 39 | if (err) { 40 | callback(err); 41 | } else { 42 | callback(null, res.statusCode, decoded && JSON.parse(decoded)); 43 | } 44 | }); 45 | } else { 46 | callback(null, res.statusCode, buffer.length > 0 ? JSON.parse(buffer) : null); 47 | } 48 | }); 49 | 50 | req.on('error', err => callback(err)); 51 | }); 52 | 53 | if (data) { 54 | req.write(data); 55 | } 56 | req.end(); 57 | } catch(err) { 58 | callback(err); 59 | } 60 | } 61 | 62 | 63 | function main() { 64 | 65 | const path = 'BUILD_NUMBER/BUILD_NUMBER'; 66 | const prefix = env.INPUT_PREFIX ? `${env.INPUT_PREFIX}-` : ''; 67 | const deletePreviousTag = env.INPUT_DELETE_PREVIOUS_TAG !== 'false'; 68 | 69 | //See if we've already generated the build number and are in later steps... 70 | if (fs.existsSync(path)) { 71 | let buildNumber = fs.readFileSync(path); 72 | console.log(`Build number already generated in earlier jobs, using build number ${buildNumber}...`); 73 | //Setting the output and a environment variable to new build number... 74 | fs.writeFileSync(process.env.GITHUB_OUTPUT, `build_number=${buildNumber}`); 75 | fs.writeFileSync(process.env.GITHUB_ENV, `BUILD_NUMBER=${buildNumber}`); 76 | return; 77 | } 78 | 79 | //Some sanity checking: 80 | for (let varName of ['INPUT_TOKEN', 'GITHUB_REPOSITORY', 'GITHUB_SHA']) { 81 | if (!env[varName]) { 82 | fail(`ERROR: Environment variable ${varName} is not defined.`); 83 | } 84 | } 85 | 86 | request('GET', `/repos/${env.GITHUB_REPOSITORY}/git/refs/tags/${prefix}build-number-`, null, (err, status, result) => { 87 | 88 | let nextBuildNumber, nrTags; 89 | 90 | if (status === 404) { 91 | console.log('No build-number ref available, starting at 1.'); 92 | nextBuildNumber = 1; 93 | nrTags = []; 94 | } else if (status === 200) { 95 | const regexString = `/${prefix}build-number-(\\d+)$`; 96 | const regex = new RegExp(regexString); 97 | nrTags = result.filter(d => d.ref.match(regex)); 98 | 99 | const MAX_OLD_NUMBERS = 5; //One or two ref deletes might fail, but if we have lots then there's something wrong! 100 | if (nrTags.length > MAX_OLD_NUMBERS) { 101 | fail(`ERROR: Too many ${prefix}build-number- refs in repository, found ${nrTags.length}, expected only 1. Check your tags!`); 102 | } 103 | 104 | //Existing build numbers: 105 | let nrs = nrTags.map(t => parseInt(t.ref.match(/-(\d+)$/)[1])); 106 | 107 | let currentBuildNumber = Math.max(...nrs); 108 | console.log(`Last build nr was ${currentBuildNumber}.`); 109 | 110 | nextBuildNumber = currentBuildNumber + 1; 111 | console.log(`Updating build counter to ${nextBuildNumber}...`); 112 | } else { 113 | if (err) { 114 | fail(`Failed to get refs. Error: ${err}, status: ${status}`); 115 | } else { 116 | fail(`Getting build-number refs failed with http status ${status}, error: ${JSON.stringify(result)}`); 117 | } 118 | } 119 | 120 | let newRefData = { 121 | ref:`refs/tags/${prefix}build-number-${nextBuildNumber}`, 122 | sha: env.GITHUB_SHA 123 | }; 124 | 125 | request('POST', `/repos/${env.GITHUB_REPOSITORY}/git/refs`, newRefData, (err, status, result) => { 126 | if (status !== 201 || err) { 127 | fail(`Failed to create new build-number ref. Status: ${status}, err: ${err}, result: ${JSON.stringify(result)}`); 128 | } 129 | 130 | console.log(`Successfully updated build number to ${nextBuildNumber}`); 131 | 132 | //Setting the output and a environment variable to new build number... 133 | fs.writeFileSync(process.env.GITHUB_OUTPUT, `build_number=${nextBuildNumber}`); 134 | fs.writeFileSync(process.env.GITHUB_ENV, `BUILD_NUMBER=${nextBuildNumber}`); 135 | 136 | //Save to file so it can be used for next jobs... 137 | fs.writeFileSync('BUILD_NUMBER', nextBuildNumber.toString()); 138 | 139 | //Cleanup 140 | if (nrTags && deletePreviousTag) { 141 | console.log(`Deleting ${nrTags.length} older build counters...`); 142 | 143 | for (let nrTag of nrTags) { 144 | request('DELETE', `/repos/${env.GITHUB_REPOSITORY}/git/${nrTag.ref}`, null, (err, status, result) => { 145 | if (status !== 204 || err) { 146 | console.warn(`Failed to delete ref ${nrTag.ref}, status: ${status}, err: ${err}, result: ${JSON.stringify(result)}`); 147 | } else { 148 | console.log(`Deleted ${nrTag.ref}`); 149 | } 150 | }); 151 | } 152 | } else if (nrTags && !deletePreviousTag) { 153 | console.log('Skipping deletion of previous build-number tags as requested.'); 154 | } 155 | 156 | }); 157 | }); 158 | } 159 | 160 | main(); 161 | 162 | 163 | 164 | --------------------------------------------------------------------------------