├── .gitignore ├── src ├── main.ts └── update-majorver.ts ├── action.yml ├── .github ├── workflows │ ├── test.yml │ ├── update-majorver.yml │ └── create-release.yml └── renovate.json5 ├── .eslintrc.js ├── LICENSE ├── README.md ├── package.json ├── tests └── update-majorver.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | out 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import run from "./update-majorver"; 2 | 3 | if (require.main === module) { 4 | run(); 5 | } 6 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Update Major Version 2 | description: Update major version tags for GitHub Actions based on semantic versioning tags 3 | author: nownabe 4 | inputs: 5 | github_token: 6 | description: GitHub token 7 | default: ${{ github.token }} 8 | runs: 9 | using: node16 10 | main: dist/index.js 11 | branding: 12 | icon: tag 13 | color: red 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Test 18 | run: npm test 19 | -------------------------------------------------------------------------------- /.github/workflows/update-majorver.yml: -------------------------------------------------------------------------------- 1 | name: Update Major Version Tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | update-majorver: 10 | name: Update Major Version Tag 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: nowactions/update-majorver@v1 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | jest: true 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | "prettier/@typescript-eslint" 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | ecmaVersion: 2018 17 | }, 18 | plugins: ["@typescript-eslint"] 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | create-release: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/create-release@v1 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | tag_name: ${{ github.ref }} 18 | release_name: ${{ github.ref }} 19 | body: Release ${{ github.ref }} 20 | draft: false 21 | prerelease: false 22 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:base", 5 | ":prHourlyLimitNone", 6 | ], 7 | dependencyDashboard: true, 8 | labels: ["renovate"], 9 | packageRules: [ 10 | { 11 | matchUpdateTypes: ["pin"], 12 | automerge: true, 13 | }, 14 | { 15 | matchFiles: ["package.json"], 16 | matchUpdateTypes: ["minor"], 17 | automerge: true, 18 | stabilityDays: 7, 19 | }, 20 | { 21 | matchFiles: ["package.json"], 22 | matchUpdateTypes: ["patch"], 23 | automerge: true, 24 | stabilityDays: 3, 25 | }, 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nownabe 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 | -------------------------------------------------------------------------------- /src/update-majorver.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { GitHub, context } from "@actions/github"; 3 | 4 | export default async function run(): Promise { 5 | try { 6 | const token = core.getInput("github_token"); 7 | const octokit = new GitHub(token); 8 | 9 | if (!context.ref.match(/^refs\/tags\/.+$/)) { 10 | core.setFailed("ref is not a tag"); 11 | return; 12 | } 13 | 14 | if (!context.ref.match(/^refs\/tags\/v\d+\.\d+\.\d+$/)) { 15 | core.setFailed("tags require semantic versioning format like v1.2.3"); 16 | return; 17 | } 18 | 19 | const tag = context.ref.split("/")[2]; 20 | const major = tag.split(".")[0]; 21 | const sha = context.payload.head_commit.id; 22 | 23 | const getRefParams = { 24 | owner: context.repo.owner, 25 | repo: context.repo.repo, 26 | ref: `tags/${major}`, 27 | }; 28 | 29 | let ref; 30 | try { 31 | ref = await octokit.git.getRef(getRefParams); 32 | core.info(`tag ${major} already exists`); 33 | } catch (error) { 34 | core.info(`tag ${major} does not exist yet`); 35 | } 36 | 37 | if (ref) { 38 | await octokit.git.updateRef({ 39 | ...getRefParams, 40 | sha, 41 | force: true, 42 | }); 43 | } else { 44 | await octokit.git.createRef({ 45 | ...getRefParams, 46 | sha, 47 | ref: `refs/tags/${major}`, 48 | }); 49 | } 50 | } catch (error) { 51 | core.setFailed(error.message); 52 | } 53 | return; 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action: Update Major Version Tag 2 | 3 | [![Test](https://github.com/nowactions/update-majorver/workflows/Test/badge.svg)](https://github.com/nowactions/update-majorver/actions) 4 | [![Release](https://img.shields.io/github/release/nowactions/update-majorver.svg)](https://github.com/nowactions/update-majorver/releases) 5 | [![License](https://img.shields.io/github/license/nowactions/update-majorver)](LICENSE) 6 | 7 | This GitHub Action updates major version tags (e.g. v1, v2) when semantic versioning tag is pushed. 8 | If `v1.2.3` tag is pushed, it updates `v1` tag. 9 | It works well with [GitHub Action versioning](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-actions#versioning-your-action). 10 | 11 | ## Usage 12 | 13 | ### Prerequisites 14 | 15 | Create a workflow `.yml` file in your `.github/workflows` directory. 16 | An [example workflow](#example) is available below. 17 | For more information, reference the GitHub Help Documentation for [Creating a workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file). 18 | 19 | ## Example 20 | 21 | ```yml 22 | name: Update Major Version Tag 23 | 24 | on: 25 | push: 26 | tags: 27 | - "v*" 28 | 29 | jobs: 30 | update-majorver: 31 | name: Update Major Version Tag 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: nowactions/update-majorver@v1 35 | ``` 36 | 37 | ## Development 38 | 39 | Install dependencies. 40 | 41 | ```bash 42 | npm install 43 | ``` 44 | 45 | Run tests. 46 | 47 | ```bash 48 | npm test 49 | ``` 50 | 51 | ### Release 52 | 53 | * Bump up the version in `package.json` 54 | * Commit the changes 55 | * Run `npm run release` 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "update-majorver", 3 | "version": "1.1.0", 4 | "description": "Update major version tags based on semver for GitHub Actions", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "lint": "eslint 'src/**.ts' 'tests/**.ts' --fix", 8 | "test": "eslint 'src/**.ts' 'tests/**.ts' && tsc --noEmit && jest --coverage", 9 | "build": "tsc", 10 | "precommit": "npm run build && git add dist/", 11 | "release": "PACKAGE_VERSION=v$(grep -m1 version package.json | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag -a $PACKAGE_VERSION -m $PACKAGE_VERSION && git push --tags" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/nowactions/update-majorver" 16 | }, 17 | "keywords": [ 18 | "actions", 19 | "node" 20 | ], 21 | "author": "nownabe", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/nowactions/update-majorver/issues" 25 | }, 26 | "homepage": "https://github.com/nowactions/update-majorver#readme", 27 | "dependencies": { 28 | "@actions/core": "^1.2.6", 29 | "@actions/github": "^2.2.0" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "26.0.24", 33 | "@typescript-eslint/eslint-plugin": "2.34.0", 34 | "@typescript-eslint/parser": "2.34.0", 35 | "@zeit/ncc": "0.22.3", 36 | "eslint": "6.8.0", 37 | "eslint-config-prettier": "6.15.0", 38 | "eslint-plugin-import": "2.32.0", 39 | "eslint-plugin-prettier": "3.4.1", 40 | "husky": "4.3.8", 41 | "jest": "26.6.3", 42 | "prettier": "2.8.8", 43 | "ts-jest": "25.5.1", 44 | "typescript": "3.9.10" 45 | }, 46 | "jest": { 47 | "testEnvironment": "node", 48 | "collectCoverageFrom": [ 49 | "src/update-majorver.js" 50 | ], 51 | "coverageThreshold": { 52 | "global": { 53 | "branches": 80, 54 | "functions": 80, 55 | "lines": 80, 56 | "statements": 80 57 | } 58 | }, 59 | "moduleFileExtensions": [ 60 | "js", 61 | "ts" 62 | ], 63 | "transform": { 64 | "^.+\\.ts$": "ts-jest" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/update-majorver.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-explicit-any: 0 */ 2 | /* eslint @typescript-eslint/camelcase: 0 */ 3 | 4 | import * as core from "@actions/core"; 5 | import { GitHub, context } from "@actions/github"; 6 | import run from "../src/update-majorver"; 7 | 8 | import { mocked } from "ts-jest/utils"; 9 | 10 | jest.mock("@actions/core"); 11 | jest.mock("@actions/github"); 12 | 13 | const mockedContext = mocked(context, true); 14 | 15 | describe("Update Major Version", () => { 16 | let updateRef: jest.Mock; 17 | let createRef: jest.Mock; 18 | 19 | beforeEach(() => { 20 | jest.spyOn(core, "getInput").mockReturnValueOnce("mygithubtoken"); 21 | context.ref = "refs/tags/v1.2.3"; 22 | mockedContext.repo = { owner: "nowactions", repo: "update-majorver" }; 23 | context.payload = { head_commit: { id: "commit_sha" } }; 24 | 25 | updateRef = jest.fn(); 26 | createRef = jest.fn(); 27 | }); 28 | 29 | test("Create a new major tag", async () => { 30 | const github = { 31 | git: { 32 | getRef: async (): Promise => { 33 | throw "error"; 34 | }, 35 | createRef, 36 | }, 37 | }; 38 | mocked(GitHub as any).mockImplementation(() => github); 39 | 40 | await run(); 41 | 42 | expect(createRef).toHaveBeenCalledWith({ 43 | owner: "nowactions", 44 | repo: "update-majorver", 45 | ref: "refs/tags/v1", 46 | sha: "commit_sha", 47 | }); 48 | }); 49 | 50 | test("Update an existing major tag", async () => { 51 | const github = { 52 | git: { 53 | getRef: async (): Promise => 54 | new Promise((resolve) => resolve(true)), 55 | updateRef, 56 | }, 57 | }; 58 | mocked(GitHub as any).mockImplementation(() => github); 59 | 60 | await run(); 61 | 62 | expect(updateRef).toHaveBeenCalledWith({ 63 | owner: "nowactions", 64 | repo: "update-majorver", 65 | ref: "tags/v1", 66 | sha: "commit_sha", 67 | force: true, 68 | }); 69 | }); 70 | 71 | test("Fails", async () => { 72 | const github = { 73 | git: { 74 | getRef: async (): Promise => 75 | new Promise((resolve) => resolve(true)), 76 | updateRef: jest.fn().mockRejectedValue(new Error("error")), 77 | }, 78 | }; 79 | mocked(GitHub as any).mockImplementation(() => github); 80 | const spy = jest.spyOn(core, "setFailed"); 81 | 82 | await run(); 83 | 84 | expect(spy).toHaveBeenCalledWith("error"); 85 | }); 86 | 87 | test("Fails with not tag reference", async () => { 88 | context.ref = "refs/heads/master"; 89 | (core as any).setFailed = jest.fn(); 90 | 91 | await run(); 92 | 93 | expect(core.setFailed).toHaveBeenCalledWith("ref is not a tag"); 94 | }); 95 | 96 | test("Fails with not semantic versioning tag", async () => { 97 | context.ref = "refs/tags/v1.2"; 98 | const spy = jest.spyOn(core, "setFailed"); 99 | 100 | await run(); 101 | 102 | expect(spy).toHaveBeenCalledWith( 103 | "tags require semantic versioning format like v1.2.3" 104 | ); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./out" /* Redirect output structure to the directory. */, 15 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */, 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "exclude": ["node_modules", "**/*.test.ts"] 61 | } 62 | --------------------------------------------------------------------------------