diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e116932..7983787 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,6 @@ jobs: strategy: matrix: base: - - core - core18 - core20 - core22 @@ -33,20 +32,58 @@ jobs: - '' - 'true' - 'false' + runner: + - ubuntu-latest include: + - base: core + arch: '' + usePodman: 'false' + runner: ubuntu-20.04 + - base: core + arch: '' + usePodman: 'true' + runner: ubuntu-20.04 - base: core arch: i386 usePodman: 'false' + runner: ubuntu-20.04 - base: core arch: i386 usePodman: 'true' + runner: ubuntu-20.04 + - base: core + arch: amd64 + usePodman: 'false' + runner: ubuntu-20.04 + - base: core + arch: amd64 + usePodman: 'true' + runner: ubuntu-20.04 + - base: core + arch: armhf + usePodman: 'false' + runner: ubuntu-20.04 + - base: core + arch: armhf + usePodman: 'true' + runner: ubuntu-20.04 + - base: core + arch: arm64 + usePodman: 'false' + runner: ubuntu-20.04 + - base: core + arch: arm64 + usePodman: 'true' + runner: ubuntu-20.04 - base: core18 arch: i386 usePodman: 'false' + runner: ubuntu-latest - base: core18 arch: i386 usePodman: 'true' - runs-on: ubuntu-latest + runner: ubuntu-latest + runs-on: ${{ matrix.runner }} steps: - uses: docker/setup-qemu-action@v2 - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 083f3ae..ca5b57d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ notes ----- * `s390x` is broken at the moment. -* `core20` & `core22` builds do not support `i386` architecture because Ubuntu has dropped support for `i386` in Ubuntu 20.04 and later. +* Builds for `core20`, and later, do not support `i386` architecture because Ubuntu has dropped support for `i386` in Ubuntu 20.04 and later. * `core` builds do not support `s390x` architecture because Ubuntu does not have support for `s390x` before Ubuntu 18.04. ## Action inputs @@ -143,3 +143,27 @@ to indicate an alternative architecture from any of those supported by the `snapcraft` utility. At the time of writing the supported architectures are `amd64`, `i386`, `arm64`, `armhf`, `ppc64el` and `s390x`. This is most-useful when used with GitHub Actions' `matrix` feature. + +### `environment` + +Add environment variables to the Snapcraft build context. Each +variable needs to be specified on a separate line. For example: + +```yaml +with: + environment: | + FOO=bar + BAZ=qux +``` +### `store-auth` + +Set the `SNAPCRAFT_STORE_CREDENTIALS` environment variable. This +is useful when using the `snapcraft push` command. + +You should not save the token into the yaml file directly, but use +the GitHub Actions secrets feature: + +```yaml +with: + store-auth: ${{ secrets.STORE_AUTH }} +``` \ No newline at end of file diff --git a/__tests__/build.test.ts b/__tests__/build.test.ts index 6fda4c3..e1d0cd2 100644 --- a/__tests__/build.test.ts +++ b/__tests__/build.test.ts @@ -8,7 +8,7 @@ import * as exec from '@actions/exec' import * as build from '../src/build' import * as tools from '../src/tools' -const default_base = 'core20' +const default_base = 'core22' afterEach(() => { jest.restoreAllMocks() @@ -21,8 +21,9 @@ test('SnapcraftBuilder expands tilde in project root', () => { 'stable', '', '', - '', - false + [], + false, + '' ) expect(builder.projectRoot).toBe(os.homedir()) @@ -32,8 +33,9 @@ test('SnapcraftBuilder expands tilde in project root', () => { 'stable', '', '', - '', - false + [], + false, + '' ) expect(builder.projectRoot).toBe(path.join(os.homedir(), 'foo/bar')) }) @@ -65,7 +67,7 @@ for (const base of ['core', 'core18', 'core20', 'core22']) { } for (const [base, arch, channel] of matrix) { test(`SnapcraftBuilder.build runs a snap build using Docker with base: ${base}; and arch: ${arch}`, async () => { - expect.assertions(3) + expect.assertions(4) const ensureDockerExperimentalMock = jest .spyOn(tools, 'ensureDockerExperimental') @@ -75,6 +77,9 @@ for (const [base, arch, channel] of matrix) { .mockImplementation( async (projectRoot: string): Promise => Promise.resolve(base) ) + const detectCGroupsV1Mock = jest + .spyOn(tools, 'detectCGroupsV1') + .mockImplementation(async (): Promise => Promise.resolve(true)) const execMock = jest .spyOn(exec, 'exec') .mockImplementation( @@ -90,8 +95,9 @@ for (const [base, arch, channel] of matrix) { 'stable', '', arch, - '', - false + [], + false, + '' ) await builder.build() @@ -102,6 +108,11 @@ for (const [base, arch, channel] of matrix) { expect(ensureDockerExperimentalMock).toHaveBeenCalled() expect(detectBaseMock).toHaveBeenCalled() + if (base === 'core') { + expect(detectCGroupsV1Mock).toHaveBeenCalled() + } else { + expect(detectCGroupsV1Mock).not.toHaveBeenCalled() + } expect(execMock).toHaveBeenCalledWith( 'docker', [ @@ -130,8 +141,7 @@ for (const [base, arch, channel] of matrix) { }) test(`SnapcraftBuilder.build runs a snap build using Podman with base: ${base}; and arch: ${arch}`, async () => { - expect.assertions(3) - + expect.assertions(4) const ensureDockerExperimentalMock = jest .spyOn(tools, 'ensureDockerExperimental') .mockImplementation(async (): Promise => Promise.resolve()) @@ -140,6 +150,9 @@ for (const [base, arch, channel] of matrix) { .mockImplementation( async (projectRoot: string): Promise => Promise.resolve(base) ) + const detectCGroupsV1Mock = jest + .spyOn(tools, 'detectCGroupsV1') + .mockImplementation(async (): Promise => Promise.resolve(true)) const execMock = jest .spyOn(exec, 'exec') .mockImplementation( @@ -155,13 +168,19 @@ for (const [base, arch, channel] of matrix) { 'stable', '', arch, - '', - true + [], + true, + '' ) await builder.build() expect(ensureDockerExperimentalMock).not.toHaveBeenCalled() expect(detectBaseMock).toHaveBeenCalled() + if (base === 'core') { + expect(detectCGroupsV1Mock).toHaveBeenCalled() + } else { + expect(detectCGroupsV1Mock).not.toHaveBeenCalled() + } expect(execMock).toHaveBeenCalledWith( 'sudo podman', [ @@ -214,8 +233,9 @@ test('SnapcraftBuilder.build can disable build info', async () => { 'stable', '', '', - '', - false + [], + false, + '' ) await builder.build() @@ -266,8 +286,62 @@ test('SnapcraftBuilder.build can pass additional arguments', async () => { 'stable', '--foo --bar', '', + [], + false, + '' + ) + await builder.build() + + expect(execMock).toHaveBeenCalledWith( + 'docker', + [ + 'run', + '--rm', + '--tty', + '--privileged', + '--volume', + `${process.cwd()}:/data`, + '--workdir', + '/data', + '--env', + `SNAPCRAFT_IMAGE_INFO={"build_url":"https://github.com/user/repo/actions/runs/42"}`, + '--env', + 'USE_SNAPCRAFT_CHANNEL=stable', + `diddledani/snapcraft:${default_base}`, + 'snapcraft', + '--foo', + '--bar' + ], + expect.anything() + ) +}) + +test('SnapcraftBuilder.build can pass extra environment variables', async () => { + expect.assertions(1) + + const ensureDockerExperimentalMock = jest + .spyOn(tools, 'ensureDockerExperimental') + .mockImplementation(async (): Promise => Promise.resolve()) + const detectBaseMock = jest + .spyOn(tools, 'detectBase') + .mockImplementation( + async (projectRoot: string): Promise => default_base + ) + const execMock = jest + .spyOn(exec, 'exec') + .mockImplementation( + async (program: string, args?: string[]): Promise => 0 + ) + + const builder = new build.SnapcraftBuilder( + '.', + false, + 'stable', + '--foo --bar', '', - false + ['FOO=bar', 'BAZ=qux'], + false, + '' ) await builder.build() @@ -283,6 +357,10 @@ test('SnapcraftBuilder.build can pass additional arguments', async () => { '--workdir', '/data', '--env', + 'FOO=bar', + '--env', + 'BAZ=qux', + '--env', `SNAPCRAFT_IMAGE_INFO={"build_url":"https://github.com/user/repo/actions/runs/42"}`, '--env', 'USE_SNAPCRAFT_CHANNEL=stable', @@ -295,6 +373,61 @@ test('SnapcraftBuilder.build can pass additional arguments', async () => { ) }) +test('SnapcraftBuilder.build adds store credentials', async () => { + expect.assertions(1) + + const ensureDockerExperimentalMock = jest + .spyOn(tools, 'ensureDockerExperimental') + .mockImplementation(async (): Promise => Promise.resolve()) + const detectBaseMock = jest + .spyOn(tools, 'detectBase') + .mockImplementation( + async (projectRoot: string): Promise => default_base + ) + const execMock = jest + .spyOn(exec, 'exec') + .mockImplementation( + async (program: string, args?: string[]): Promise => 0 + ) + + const builder = new build.SnapcraftBuilder( + '.', + false, + 'stable', + '--foo --bar', + '', + [], + false, + 'TEST_STORE_CREDENTIALS' + ) + await builder.build() + + expect(execMock).toHaveBeenCalledWith( + 'docker', + [ + 'run', + '--rm', + '--tty', + '--privileged', + '--volume', + `${process.cwd()}:/data`, + '--workdir', + '/data', + '--env', + `SNAPCRAFT_IMAGE_INFO={"build_url":"https://github.com/user/repo/actions/runs/42"}`, + '--env', + 'USE_SNAPCRAFT_CHANNEL=stable', + '--env', + 'SNAPCRAFT_STORE_CREDENTIALS=TEST_STORE_CREDENTIALS', + `diddledani/snapcraft:${default_base}`, + 'snapcraft', + '--foo', + '--bar' + ], + expect.anything() + ) +}) + test('SnapcraftBuilder.outputSnap fails if there are no snaps', async () => { expect.assertions(2) @@ -305,8 +438,9 @@ test('SnapcraftBuilder.outputSnap fails if there are no snaps', async () => { 'stable', '', '', - '', - false + [], + false, + '' ) const readdir = jest @@ -331,8 +465,9 @@ test('SnapcraftBuilder.outputSnap returns the first snap', async () => { 'stable', '', '', - '', - false + [], + false, + '' ) const readdir = jest diff --git a/action.yml b/action.yml index 83b0078..f8b503f 100644 --- a/action.yml +++ b/action.yml @@ -64,6 +64,25 @@ inputs: combine this with the build matrix feature of GitHub Actions. default: 'amd64' required: true + environment: + description: > + Environment to pass to Snapcraft + + Add environment variables to the Snapcraft build context. Each + variable needs to be specified on a separate line. For example: + + environment: | + FOO=bar + BAZ=qux + required: false + store-auth: + description: > + The Snap Store authentication token + + This token is used to authenticate with the Snap Store when + uploading the snap. It can be obtained by running `snapcraft + export-login --snaps -` and copying the output. + required: false outputs: snap: description: 'The file name of the resulting snap.' diff --git a/dist/index.js b/dist/index.js index d691aa9..5b4a514 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7957,77 +7957,64 @@ var jsYaml = { ;// CONCATENATED MODULE: ./lib/tools.js // -*- mode: javascript; js-indent-level: 2 -*- -var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; const dockerJson = '/etc/docker/daemon.json'; -function haveFile(filePath) { - return __awaiter(this, void 0, void 0, function* () { - try { - yield external_fs_.promises.access(filePath, external_fs_.constants.R_OK); - } - catch (err) { - return false; - } - return true; - }); +async function haveFile(filePath) { + try { + await external_fs_.promises.access(filePath, external_fs_.constants.R_OK); + } + catch (err) { + return false; + } + return true; } -function ensureDockerExperimental() { - return __awaiter(this, void 0, void 0, function* () { - let json = {}; - if (yield haveFile(dockerJson)) { - json = JSON.parse(yield external_fs_.promises.readFile(dockerJson, { encoding: 'utf-8' })); - } - if (!('experimental' in json) || json['experimental'] !== true) { - json['experimental'] = true; - yield exec.exec('bash', [ - '-c', - `echo '${JSON.stringify(json)}' | sudo tee /etc/docker/daemon.json` - ]); - yield exec.exec('sudo', ['systemctl', 'restart', 'docker']); +async function ensureDockerExperimental() { + let json = {}; + if (await haveFile(dockerJson)) { + json = JSON.parse(await external_fs_.promises.readFile(dockerJson, { encoding: 'utf-8' })); + } + if (!('experimental' in json) || json['experimental'] !== true) { + json['experimental'] = true; + await exec.exec('bash', [ + '-c', + `echo '${JSON.stringify(json)}' | sudo tee /etc/docker/daemon.json` + ]); + await exec.exec('sudo', ['systemctl', 'restart', 'docker']); + } +} +async function findSnapcraftYaml(projectRoot) { + const filePaths = [ + external_path_.join(projectRoot, 'snap', 'snapcraft.yaml'), + external_path_.join(projectRoot, 'snapcraft.yaml'), + external_path_.join(projectRoot, '.snapcraft.yaml') + ]; + for (const filePath of filePaths) { + if (await haveFile(filePath)) { + return filePath; } - }); + } + throw new Error('Cannot find snapcraft.yaml'); } -function findSnapcraftYaml(projectRoot) { - return __awaiter(this, void 0, void 0, function* () { - const filePaths = [ - external_path_.join(projectRoot, 'snap', 'snapcraft.yaml'), - external_path_.join(projectRoot, 'snapcraft.yaml'), - external_path_.join(projectRoot, '.snapcraft.yaml') - ]; - for (const filePath of filePaths) { - if (yield haveFile(filePath)) { - return filePath; - } - } - throw new Error('Cannot find snapcraft.yaml'); - }); +async function detectBase(projectRoot) { + const snapcraftFile = await findSnapcraftYaml(projectRoot); + const snapcraftYaml = load(await external_fs_.promises.readFile(snapcraftFile, 'utf-8'), { filename: snapcraftFile }); + if (snapcraftYaml === undefined) { + throw new Error('Cannot parse snapcraft.yaml'); + } + if (snapcraftYaml['build-base']) { + return snapcraftYaml['build-base']; + } + if (snapcraftYaml.base) { + return snapcraftYaml.base; + } + return 'core'; } -function detectBase(projectRoot) { - return __awaiter(this, void 0, void 0, function* () { - const snapcraftFile = yield findSnapcraftYaml(projectRoot); - const snapcraftYaml = load(yield external_fs_.promises.readFile(snapcraftFile, 'utf-8'), { filename: snapcraftFile }); - if (snapcraftYaml === undefined) { - throw new Error('Cannot parse snapcraft.yaml'); - } - if (snapcraftYaml['build-base']) { - return snapcraftYaml['build-base']; - } - if (snapcraftYaml.base) { - return snapcraftYaml.base; - } - return 'core'; - }); +async function detectCGroupsV1() { + const cgroups = await external_fs_.promises.readFile('/proc/1/cgroup', 'utf-8'); + return cgroups.includes('cpu,cpuacct'); } ;// CONCATENATED MODULE: ./lib/argparser.js @@ -8103,15 +8090,6 @@ function getChannel(base, channel) { ;// CONCATENATED MODULE: ./lib/build.js // -*- mode: javascript; js-indent-level: 2 -*- -var build_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; @@ -8139,139 +8117,128 @@ const platforms = { s390x: 'linux/s390x' }; class SnapcraftBuilder { - constructor(projectRoot, includeBuildInfo, snapcraftChannel, snapcraftArgs, architecture, environment, usePodman) { + constructor(projectRoot, includeBuildInfo, snapcraftChannel, snapcraftArgs, architecture, environment, usePodman, storeAuth) { this.projectRoot = expandHome(projectRoot); this.includeBuildInfo = includeBuildInfo; this.snapcraftChannel = snapcraftChannel; this.snapcraftArgs = parseArgs(snapcraftArgs); this.architecture = architecture; this.usePodman = usePodman; - const envs = parseArgs(environment); + this.storeAuth = storeAuth; const envKV = {}; - for (const env of envs) { + for (const env of environment) { const [key, value] = env.split('=', 2); envKV[key] = value; } this.environment = envKV; } - build() { - return build_awaiter(this, void 0, void 0, function* () { - if (!this.usePodman) { - yield ensureDockerExperimental(); - } - const base = yield detectBase(this.projectRoot); - if (!['core', 'core18', 'core20', 'core22'].includes(base)) { - throw new Error(`Your build requires a base that this tool does not support (${base}). 'base' or 'build-base' in your 'snapcraft.yaml' must be one of 'core', 'core18' or 'core20'.`); - } - const imageInfo = { - // eslint-disable-next-line @typescript-eslint/naming-convention - build_url: `https://github.com/${external_process_namespaceObject.env.GITHUB_REPOSITORY}/actions/runs/${external_process_namespaceObject.env.GITHUB_RUN_ID}` - }; - // Copy and update environment to pass to snapcraft - const env = this.environment; - env['SNAPCRAFT_IMAGE_INFO'] = JSON.stringify(imageInfo); - if (this.includeBuildInfo) { - env['SNAPCRAFT_BUILD_INFO'] = '1'; - } - if (this.snapcraftChannel !== '') { - env['USE_SNAPCRAFT_CHANNEL'] = getChannel(base, this.snapcraftChannel); - } - let dockerArgs = []; - if (this.architecture in platforms && !this.usePodman) { - dockerArgs = dockerArgs.concat('--platform', platforms[this.architecture]); - } - for (const key in env) { - dockerArgs = dockerArgs.concat('--env', `${key}=${env[key]}`); - } - let command = 'docker'; - let containerImage = `diddledani/snapcraft:${base}`; - if (this.usePodman) { - command = 'sudo podman'; - containerImage = `docker.io/${containerImage}`; - dockerArgs = dockerArgs.concat('--systemd', 'always'); - } - yield exec.exec(command, [ - 'run', - '--rm', - '--tty', - '--privileged', - '--volume', - `${this.projectRoot}:/data`, - '--workdir', - '/data', - ...dockerArgs, - containerImage, - 'snapcraft', - ...this.snapcraftArgs - ], { - cwd: this.projectRoot - }); + async build() { + if (!this.usePodman) { + await ensureDockerExperimental(); + } + const base = await detectBase(this.projectRoot); + if (!['core', 'core18', 'core20', 'core22'].includes(base)) { + throw new Error(`Your build requires a base that this tool does not support (${base}). 'base' or 'build-base' in your 'snapcraft.yaml' must be one of 'core', 'core18' or 'core20'.`); + } + if (base === 'core' && !(await detectCGroupsV1())) { + throw new Error(`Your build specified 'core' as the base, but your system is using cgroups v2. 'core' does not support cgroups v2. Please use 'core18' or later or an older Linux distribution that uses CGroups version 1 instead.`); + } + const imageInfo = { + // eslint-disable-next-line @typescript-eslint/naming-convention + build_url: `https://github.com/${external_process_namespaceObject.env.GITHUB_REPOSITORY}/actions/runs/${external_process_namespaceObject.env.GITHUB_RUN_ID}` + }; + // Copy and update environment to pass to snapcraft + const env = this.environment; + env['SNAPCRAFT_IMAGE_INFO'] = JSON.stringify(imageInfo); + if (this.includeBuildInfo) { + env['SNAPCRAFT_BUILD_INFO'] = '1'; + } + if (this.snapcraftChannel !== '') { + env['USE_SNAPCRAFT_CHANNEL'] = getChannel(base, this.snapcraftChannel); + } + if (this.storeAuth !== '') { + env['SNAPCRAFT_STORE_CREDENTIALS'] = this.storeAuth; + } + let dockerArgs = []; + if (this.architecture in platforms && !this.usePodman) { + dockerArgs = dockerArgs.concat('--platform', platforms[this.architecture]); + } + for (const key in env) { + dockerArgs = dockerArgs.concat('--env', `${key}=${env[key]}`); + } + let command = 'docker'; + let containerImage = `diddledani/snapcraft:${base}`; + if (this.usePodman) { + command = 'sudo podman'; + containerImage = `docker.io/${containerImage}`; + dockerArgs = dockerArgs.concat('--systemd', 'always'); + } + await exec.exec(command, [ + 'run', + '--rm', + '--tty', + '--privileged', + '--volume', + `${this.projectRoot}:/data`, + '--workdir', + '/data', + ...dockerArgs, + containerImage, + 'snapcraft', + ...this.snapcraftArgs + ], { + cwd: this.projectRoot }); } // This wrapper is for the benefit of the tests, due to the crazy // typing of fs.promises.readdir() - _readdir(dir) { - return build_awaiter(this, void 0, void 0, function* () { - return yield external_fs_.promises.readdir(dir); - }); - } - outputSnap() { - return build_awaiter(this, void 0, void 0, function* () { - const files = yield this._readdir(this.projectRoot); - const snaps = files.filter(name => name.endsWith('.snap')); - if (snaps.length === 0) { - throw new Error('No snap files produced by build'); - } - if (snaps.length > 1) { - core.warning(`Multiple snaps found in ${this.projectRoot}`); - } - const snap = external_path_.join(this.projectRoot, snaps[0]); - yield exec.exec('sudo', ['chown', external_process_namespaceObject.getuid().toString(), snap]); - return snap; - }); + async _readdir(dir) { + return await external_fs_.promises.readdir(dir); + } + async outputSnap() { + const files = await this._readdir(this.projectRoot); + const snaps = files.filter(name => name.endsWith('.snap')); + if (snaps.length === 0) { + throw new Error('No snap files produced by build'); + } + if (snaps.length > 1) { + core.warning(`Multiple snaps found in ${this.projectRoot}`); + } + const snap = external_path_.join(this.projectRoot, snaps[0]); + await exec.exec('sudo', ['chown', external_process_namespaceObject.getuid().toString(), snap]); + return snap; } } ;// CONCATENATED MODULE: ./lib/main.js // -*- mode: javascript; js-indent-level: 2 -*- -var main_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -function run() { - var _a, _b; - return main_awaiter(this, void 0, void 0, function* () { - try { - if (external_os_.platform() !== 'linux') { - throw new Error(`Only supported on linux platform`); - } - const path = core.getInput('path'); - const usePodman = ((_a = core.getInput('use-podman')) !== null && _a !== void 0 ? _a : 'true').toUpperCase() === 'TRUE'; - const buildInfo = ((_b = core.getInput('build-info')) !== null && _b !== void 0 ? _b : 'true').toUpperCase() === 'TRUE'; - core.info(`Building Snapcraft project in "${path}"...`); - const snapcraftChannel = core.getInput('snapcraft-channel'); - const snapcraftArgs = core.getInput('snapcraft-args'); - const architecture = core.getInput('architecture'); - const environment = core.getInput('environment'); - const builder = new SnapcraftBuilder(path, buildInfo, snapcraftChannel, snapcraftArgs, architecture, environment, usePodman); - yield builder.build(); - const snap = yield builder.outputSnap(); - core.setOutput('snap', snap); - } - catch (error) { - if (error instanceof Error) { - core.setFailed(error.message); - } +async function run() { + try { + if (external_os_.platform() !== 'linux') { + throw new Error(`Only supported on linux platform`); } - }); + const path = core.getInput('path'); + const usePodman = (core.getInput('use-podman') ?? 'true').toUpperCase() === 'TRUE'; + const buildInfo = (core.getInput('build-info') ?? 'true').toUpperCase() === 'TRUE'; + core.info(`Building Snapcraft project in "${path}"...`); + const snapcraftChannel = core.getInput('snapcraft-channel'); + const snapcraftArgs = core.getInput('snapcraft-args'); + const architecture = core.getInput('architecture'); + const environment = core.getMultilineInput('environment'); + const store_auth = core.getInput('store-auth'); + const builder = new SnapcraftBuilder(path, buildInfo, snapcraftChannel, snapcraftArgs, architecture, environment, usePodman, store_auth); + await builder.build(); + const snap = await builder.outputSnap(); + core.setOutput('snap', snap); + } + catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } + } } run(); diff --git a/src/build.ts b/src/build.ts index 8795513..576b6bd 100644 --- a/src/build.ts +++ b/src/build.ts @@ -43,6 +43,7 @@ export class SnapcraftBuilder { architecture: string environment: {[key: string]: string} usePodman: boolean + storeAuth: string constructor( projectRoot: string, @@ -50,8 +51,9 @@ export class SnapcraftBuilder { snapcraftChannel: string, snapcraftArgs: string, architecture: string, - environment: string, - usePodman: boolean + environment: string[], + usePodman: boolean, + storeAuth: string ) { this.projectRoot = expandHome(projectRoot) this.includeBuildInfo = includeBuildInfo @@ -59,10 +61,10 @@ export class SnapcraftBuilder { this.snapcraftArgs = parseArgs(snapcraftArgs) this.architecture = architecture this.usePodman = usePodman + this.storeAuth = storeAuth - const envs = parseArgs(environment) const envKV: {[key: string]: string} = {} - for (const env of envs) { + for (const env of environment) { const [key, value] = env.split('=', 2) envKV[key] = value } @@ -81,6 +83,12 @@ export class SnapcraftBuilder { ) } + if (base === 'core' && !(await tools.detectCGroupsV1())) { + throw new Error( + `Your build specified 'core' as the base, but your system is using cgroups v2. 'core' does not support cgroups v2. Please use 'core18' or later or an older Linux distribution that uses CGroups version 1 instead.` + ) + } + const imageInfo: ImageInfo = { // eslint-disable-next-line @typescript-eslint/naming-convention build_url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` @@ -94,6 +102,9 @@ export class SnapcraftBuilder { if (this.snapcraftChannel !== '') { env['USE_SNAPCRAFT_CHANNEL'] = getChannel(base, this.snapcraftChannel) } + if (this.storeAuth !== '') { + env['SNAPCRAFT_STORE_CREDENTIALS'] = this.storeAuth + } let dockerArgs: string[] = [] if (this.architecture in platforms && !this.usePodman) { diff --git a/src/main.ts b/src/main.ts index 6f46197..335c766 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,7 +19,8 @@ async function run(): Promise { const snapcraftChannel = core.getInput('snapcraft-channel') const snapcraftArgs = core.getInput('snapcraft-args') const architecture = core.getInput('architecture') - const environment = core.getInput('environment') + const environment = core.getMultilineInput('environment') + const store_auth = core.getInput('store-auth') const builder = new SnapcraftBuilder( path, @@ -28,7 +29,8 @@ async function run(): Promise { snapcraftArgs, architecture, environment, - usePodman + usePodman, + store_auth ) await builder.build() const snap = await builder.outputSnap() diff --git a/src/tools.ts b/src/tools.ts index 43c8534..6dbba53 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -69,3 +69,8 @@ export async function detectBase(projectRoot: string): Promise { } return 'core' } + +export async function detectCGroupsV1(): Promise { + const cgroups = await fs.promises.readFile('/proc/1/cgroup', 'utf-8') + return cgroups.includes('cpu,cpuacct') +} diff --git a/tsconfig.json b/tsconfig.json index 40a1b6b..caf9bab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "target": "ES2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "ES2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "moduleResolution": "node", "outDir": "./lib", /* Redirect output structure to the directory. */ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */