From b000906c7a2b992601f1ed93577515da87294ce1 Mon Sep 17 00:00:00 2001 From: Reinhard Naegele Date: Tue, 19 Nov 2019 17:45:29 +0100 Subject: [PATCH] Add chart-releaser action Signed-off-by: Reinhard Naegele --- action.yml | 14 ++++++++ package-lock.json | 77 ++++++++++++++++++++++++++++++++++++++++ package.json | 22 ++++++++++++ src/cr.ts | 87 +++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++++ src/tools.ts | 61 ++++++++++++++++++++++++++++++++ tsconfig.json | 24 +++++++++++++ 7 files changed, 375 insertions(+) create mode 100644 action.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/cr.ts create mode 100644 src/main.ts create mode 100644 src/tools.ts create mode 100644 tsconfig.json diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..5d3e5dd --- /dev/null +++ b/action.yml @@ -0,0 +1,14 @@ +name: "chart-releaser action" +description: "Run the chart-releaser tool" +author: "unguiculus" +inputs: + charts-dir: + description: The charts directory + default: charts + charts-repo-url: + description: "The GitHub Pages URL to the charts repo (default: https://.github.io/)" + token: + description: The GitHub token +runs: + using: "node12" + main: "lib/main.js" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4141a07 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,77 @@ +{ + "name": "chart-testing", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@actions/core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.0.tgz", + "integrity": "sha512-ZKdyhlSlyz38S6YFfPnyNgCDZuAF2T0Qv5eHflNWytPS8Qjvz39bZFMry9Bb/dpSnqWcNeav5yM2CTYpJeY+Dw==" + }, + "@actions/exec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz", + "integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" + }, + "@actions/io": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz", + "integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA==" + }, + "@actions/tool-cache": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.1.2.tgz", + "integrity": "sha512-IJczPaZr02ECa3Lgws/TJEVco9tjOujiQSZbO3dHuXXjhd5vrUtfOgGwhmz3/f97L910OraPZ8SknofUk6RvOQ==", + "requires": { + "@actions/core": "^1.1.0", + "@actions/exec": "^1.0.1", + "@actions/io": "^1.0.1", + "semver": "^6.1.0", + "typed-rest-client": "^1.4.0", + "uuid": "^3.3.2" + } + }, + "@types/node": { + "version": "12.12.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.8.tgz", + "integrity": "sha512-XLla8N+iyfjvsa0KKV+BP/iGSoTmwxsu5Ci5sM33z9TjohF72DEz95iNvD6pPmemvbQgxAv/909G73gUn8QR7w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "tunnel": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", + "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=" + }, + "typed-rest-client": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz", + "integrity": "sha512-DVZRlmsfnTjp6ZJaatcdyvvwYwbWvR4YDNFDqb+qdTxpvaVP99YCpBkA8rxsLtAPjBVoDe4fNsnMIdZTiPuKWg==", + "requires": { + "tunnel": "0.0.4", + "underscore": "1.8.3" + } + }, + "typescript": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", + "dev": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..623d536 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "chart-testing", + "version": "0.0.0", + "description": "kind GitHub action", + "main": "lib/main.js", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "author": "The Helm Authors", + "license": "Apache", + "dependencies": { + "@actions/core": "^1.2.0", + "@actions/tool-cache": "^1.1.2", + "@actions/exec": "^1.0.1", + "@actions/io": "^1.0.1" + }, + "devDependencies": { + "@types/node": "^12.12.8", + "typescript": "^3.7.2" + } +} diff --git a/src/cr.ts b/src/cr.ts new file mode 100644 index 0000000..0e37659 --- /dev/null +++ b/src/cr.ts @@ -0,0 +1,87 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; +import {cr, git, helm} from "./tools"; + +export class ChartReleaser { + constructor(private readonly owner: string, private readonly repository: string, + private readonly chartRepoUrl: string, private readonly token: string) { + if (owner === "") { + throw new Error("owner is required") + } + if (repository === "") { + throw new Error("repository is required") + } + if (chartRepoUrl == "") { + this.chartRepoUrl = `https://${this.owner}.github.io/${this.repository}` + } + if (token === "") { + throw new Error("token is required") + } + } + + async execute(charts: Set) { + if (charts.size === 0) { + console.log("Charts set is emtpy. Nothing to do.") + return + } + + await io.rmRF(".cr-release-packages"); + await io.mkdirP(".cr-release-packages"); + + await io.rmRF(".cr-index"); + await io.mkdirP(".cr-index"); + + for (const chart of charts) { + await this.packageChart(chart) + } + + await this.releaseCharts(); + await this.updateIndex(); + } + + private async packageChart(chart: string) { + console.log(`Packacking chart '${chart}'...`); + + await helm("package", chart, "--destination", ".cr-release-packages", "--save=false", "--dependency-update"); + } + + private async releaseCharts() { + console.log("Releasing charts..."); + + await cr(this.token, "upload", "-o", this.owner, "-r", this.repository); + } + + private async updateIndex() { + console.log("Updating repo index..."); + + await cr(this.token, "index", "-o", this.owner, "-r", this.repository, "-c", this.chartRepoUrl); + + await git("checkout", "gh-pages"); + + await exec.exec("cp", ["--force", ".cr-index/index.yaml", "index.yaml"]); + + const actor = process.env["GITHUB_ACTOR"] || ""; + await git("config", "--local", "user.name", actor); + await git("config", "--local", "user.email", "noreply@github.com"); + + await git("add", "index.yaml"); + await git("commit", "--message='Update index.yaml'", "--signoff"); + + const repoUrl = `https://x-access-token:${this.token}@github.com/${this.owner}/${this.repository}`; + await git("push", repoUrl, "gh-pages"); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3446301 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,90 @@ +// Copyright The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as core from '@actions/core'; +import {ChartReleaser} from "./cr"; +import {git} from "./tools"; + +const ChartsDirInput = "charts-dir"; +const ChartsRepoUrlInput = "charts-repo-url"; +const TokenInput = "token"; + +export function createChartReleaser(): ChartReleaser { + const ownerRepo = process.env["GITHUB_REPOSITORY"] || ""; + const split = ownerRepo.split("/"); + const owner = split[0]; + const repo = split[1]; + + const chartsRepoUrl = core.getInput(ChartsRepoUrlInput); + const token = core.getInput(TokenInput); + return new ChartReleaser(owner, repo, chartsRepoUrl, token) +} + +async function run() { + try { + const workspace = process.env["GITHUB_WORKSPACE"]; + if (workspace !== process.cwd()) { + core.setFailed("action must be run in the workspace root"); + return + } + + let chartsDir = core.getInput(ChartsDirInput); + if (chartsDir === "") { + chartsDir = "charts" + } + + console.log("Looking up latest tag..."); + const tag = await findLatestTag(); + + console.log(`Identifying changed charts since ${tag}...`) + const charts = await findChangedCharts(chartsDir, tag); + + if (charts.size == 0) { + console.log("Nothing to do. No chart changes detected.") + return + } + + const cr = createChartReleaser(); + await cr.execute(charts) + } catch (error) { + core.setFailed(error.message); + } +} + +async function findLatestTag(): Promise { + let tag: string; + try { + tag = await git("describe", "--tags", "--abbrev=0"); + } catch (ex) { + console.log("No tag found in repo. Getting first commit instead..."); + tag = await git("rev-list", "--max-parents=0", "--first-parent", "HEAD"); + } + return tag.trim() +} + +export async function findChangedCharts(chartsDir: string, ref: string): Promise> { + let changes = await git("diff", "--find-renames", "--name-only", ref, "--", chartsDir); + const set = new Set(); + + changes = changes.trim(); + if (changes) { + for (let change of changes.split("\n")) { + const split = change.split("/"); + set.add(split[0] + "/" + split[1]) + } + } + return set +} + +run(); diff --git a/src/tools.ts b/src/tools.ts new file mode 100644 index 0000000..0bc2ffd --- /dev/null +++ b/src/tools.ts @@ -0,0 +1,61 @@ +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; +import fs from 'fs'; + +export async function git(...args: string[]): Promise { + let output = ''; + + const options = {}; + // @ts-ignore + options.listeners = { + stdout: (data: Buffer) => { + output += data.toString(); + } + }; + + await exec.exec("git", args, options); + return output +} + +export async function helm(...args: string[]) { + const home = process.env["HOME"]; + const workspace = process.env["GITHUB_WORKSPACE"]; + + const dockerArgs = [ + "run", + "--interactive", + "--rm", + "--volume", `${home}/.helm:/root/.helm`, + "--volume", `${workspace}:/workdir`, + "--workdir", "/workdir", + "lachlanevenson/k8s-helm:v2.16.1" + ]; + + if (!fs.existsSync(`${home}/.helm`)) { + await io.mkdirP(`${home}/.helm`); + await exec.exec("docker", dockerArgs.concat("init", "--client-only")) + } + await exec.exec("docker", dockerArgs.concat(args)); +} + +export async function cr(token: string, ...args: string[]) { + const workspace = process.env["GITHUB_WORKSPACE"]; + + const options = {}; + // @ts-ignore + options.env = { + CR_TOKEN: token + }; + + const dockerArgs = [ + "run", + "--interactive", + "--rm", + "--env", "CR_TOKEN", + "--volume", `${workspace}:/workdir`, + "--workdir", "/workdir", + "quay.io/helmpack/chart-releaser:v0.2.3" + ]; + + await exec.exec("docker", dockerArgs.concat("cr", args), options); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..746b873 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "./lib", + "rootDir": "./src", + "strict": true, + "noImplicitAny": false, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "exclude": [ + "node_modules" + ] +}