Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add option to use balena deploy as command #201

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,20 @@ jobs:

Inputs are provided using the `with:` section of your workflow YML file.

| key | Description | Required | Default |
| --- | --- | --- | --- |
| balena_token | API key to balenaCloud | true | |
| fleet | The slug of the fleet (eg: `my_org/sample_fleet`) for which the release is for | true | |
| environment | Domain of API hosting your fleets | false | balena-cloud.com |
| cache | If a release matching the commit already exists do not build again | false | true |
| versionbot | Tells action to use Versionbot branch for versioning | false | false |
| create_tag | Create a tag on the git commit with the final release version | false | false |
| source | Specify a source directory (for `Dockerfile.template` or `docker-compose.yml`) | false | root directory |
| layer_cache | Use cached layers of previously built images for this project | false | true |
| registry_secrets | JSON string containing image registry credentials used to pull base images | false | |

`balena_token` and other tokens needs to be stored in GitHub as an [encrypted secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) that GitHub Actions can access.
| key | Description | Required | Default |
| ---------------- | ------------------------------------------------------------------------------ | -------- | ---------------- |
| balena_token | API key to balenaCloud | true | |
| fleet | The slug of the fleet (eg: `my_org/sample_fleet`) for which the release is for | true | |
| environment | Domain of API hosting your fleets | false | balena-cloud.com |
| cache | If a release matching the commit already exists do not build again | false | true |
| versionbot | Tells action to use Versionbot branch for versioning | false | false |
| create_tag | Create a tag on the git commit with the final release version | false | false |
| source | Specify a source directory (for `Dockerfile.template` or `docker-compose.yml`) | false | root directory |
| layer_cache | Use cached layers of previously built images for this project | false | true |
| registry_secrets | JSON string containing image registry credentials used to pull base images | false | |
| use_deploy | Use `balena deploy` instead of `balena push` for creating the release | false | |

`balena_token` and other tokens needs to be stored in GitHub as an [encrypted secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) that GitHub Actions can access.

`environment` can be used to specify a custom domain for the backend that will build and deploy your release. If for example you want to deploy to staging environment, you would set it to `balena-staging.com` or if you run your own instance of balenaCloud such as openBalena then specify your domain here.

Expand All @@ -57,18 +58,18 @@ Inputs are provided using the `with:` section of your workflow YML file.
"username": "${{ secrets.REGISTRY_USER }}",
"password": "${{ secrets.REGISTRY_PASS }}"
}
}
}
```

## Outputs

| key | Description | Nullable |
| --- | --- | --- |
| release_id | ID of the release built | true |
| version | Version of the release built | true |
| key | Description | Nullable |
| ---------- | ---------------------------- | -------- |
| release_id | ID of the release built | true |
| version | Version of the release built | true |

The `release_id` output could be null because the action might just finalize previously built releases.

## Workflows

This action is leveraging the `is_final` trait of a release to enable you to develop releases in a way that make it easier to test.
Expand Down Expand Up @@ -96,7 +97,7 @@ on:

### Additional comments about workflows

If you need to build a release for multiple fleets across several environments (balena-cloud.com, balena-staging.com, etc) you can create multiple workflow files for each environment and use a matrix to pass a list of fleet names into 1 job. See how Balena's Supervisor does this with the [staging deployment workflow](https://github.com/balena-os/balena-supervisor/blob/caf3c1fd5867c127346058742cfa4864e9072313/.github/workflows/staging-balena-ci.yml).
If you need to build a release for multiple fleets across several environments (balena-cloud.com, balena-staging.com, etc) you can create multiple workflow files for each environment and use a matrix to pass a list of fleet names into 1 job. See how Balena's Supervisor does this with the [staging deployment workflow](https://github.com/balena-os/balena-supervisor/blob/caf3c1fd5867c127346058742cfa4864e9072313/.github/workflows/staging-balena-ci.yml).

## Development

Expand Down
10 changes: 7 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: 'Deploy to Balena'
description: 'Automate release builds and deployments'
branding:
icon: 'code'
icon: 'code'
color: 'blue'
inputs:
balena_token:
Expand Down Expand Up @@ -46,11 +46,15 @@ inputs:
layer_cache:
description: 'Use cached layers of previously built images for this project'
required: false
default: true
default: true
registry_secrets:
description: 'Image registry credentrials (Balena secrets.json)'
required: false
default: ''
default: ''
use_deploy:
description: 'Use `balena deploy` instead of `balena push` for creating the new release.'
required: false
default: false
outputs:
release_id:
description: 'ID of the release built'
Expand Down
4 changes: 3 additions & 1 deletion src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ export async function run(
};
}

const operation = inputs.useDeploy ? balena.deploy : balena.push;

// Finally send source to builders
try {
releaseId = await balena.push(inputs.fleet, inputs.source, inputs.cache, {
releaseId = await operation(inputs.fleet, inputs.source, inputs.cache, {
...buildOptions,
noCache: inputs.layerCache === false,
});
Expand Down
56 changes: 43 additions & 13 deletions src/balena-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ export async function init(endpoint: string, token: string) {
await sdk.auth.loginWithToken(token);
}

export async function push(
async function pushOrDeploy(
fleet: string,
source: string,
useCache: boolean,
useDeploy = false,
options: Partial<BuildOptions>,
): Promise<Release['id']> {
if (process.env.GITHUB_ACTIONS === 'false') {
Expand Down Expand Up @@ -77,12 +78,13 @@ export async function push(
}
}

const pushOpt = [
'push',
const cliOpts = [
useDeploy ? 'deploy' : 'push',
fleet,
'--source',
source,
'--release-tag',
...(useDeploy ? ['--projectName', fleet] : []),
...Object.entries(buildOpt.tags).flatMap(([key, value]) => [
TagKeyMap[key as keyof typeof TagKeyMap],
typeof value === 'string' && value.includes(' ')
Expand All @@ -92,25 +94,25 @@ export async function push(
];

if (buildOpt.draft) {
pushOpt.push('--draft');
cliOpts.push('--draft');
}

if (buildOpt.noCache) {
pushOpt.push('--nocache');
cliOpts.push('--nocache');
}

let releaseId: string | null = null;

return new Promise((resolve, reject) => {
core.debug(`balena ${pushOpt.join(' ')}`);
core.debug(`balena ${cliOpts.join(' ')}`);

const buildProcess = spawn('balena', pushOpt, {
const cliProcess = spawn('balena', cliOpts, {
stdio: 'pipe',
});

buildProcess.stdout.setEncoding('utf8');
cliProcess.stdout.setEncoding('utf8');

buildProcess.stdout.on('data', (data: Buffer) => {
cliProcess.stdout.on('data', (data: Buffer) => {
const msg = stripAnsi(data.toString());
// Ignore logging messages if they are progress bar lines since they don't display correctly
if (!isProgressBar(msg) && !isEmptyCharacter(msg)) {
Expand All @@ -122,19 +124,19 @@ export async function push(
}
});

buildProcess.stderr.on('data', (data: Buffer) => {
cliProcess.stderr.on('data', (data: Buffer) => {
core.error(stripAnsi(data.toString()));
});

process.on('SIGTERM', () => {
buildProcess.kill('SIGINT');
cliProcess.kill('SIGINT');
});

process.on('SIGINT', () => {
buildProcess.kill('SIGINT');
cliProcess.kill('SIGINT');
});

buildProcess.on('exit', (code: number) => {
cliProcess.on('exit', (code: number) => {
if (code !== 0) {
return reject('Build process returned non-0 exit code');
}
Expand All @@ -148,6 +150,34 @@ export async function push(
});
}

export async function push(
fleet: string,
source: string,
useCache: boolean,
options: Partial<BuildOptions>,
): Promise<Release['id']> {
return pushOrDeploy(fleet, source, useCache, false, options);
}

export async function deploy(
fleet: string,
source: string,
useCache: boolean,
options: Partial<BuildOptions>,
): Promise<Release['id']> {
// TODO: Parse docker compose
// for services with `build`, look for a `<fleet>_<serviceName>:<arch>` image or a `<fleet>_<serviceName>:latest`
// image, where `<arch>` is the architecture deduced from the fleet. It should look for both
// balena architecture ids and docker architecture ids: e.g. `armv7hf` and `armv7` are valid tags

// TODO: use dockerode to look for the images in the local context, if they don't exist
// throw an error

// TODO: If different than `latest`, pass the `--tag` argument to deploy

return pushOrDeploy(fleet, source, useCache, true, options);
}

export async function getReleaseByTags(
fleet: string,
tags: Tags,
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const inputs: Inputs = {
source: join(WORKSPACE, core.getInput('source', { required: false })),
githubToken: core.getInput('github_token', { required: false }),
layerCache: core.getBooleanInput('layer_cache', { required: false }),
useDeploy: core.getBooleanInput('use_deploy', { required: false }),
};

(async () => {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Inputs = {
versionbot: boolean;
createTag: boolean;
layerCache: boolean;
useDeploy: boolean;
};

export type RepoContext = {
Expand Down