Skip to content

Commit

Permalink
add promote action (#138)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Aronsson <[email protected]>
  • Loading branch information
lucabello and simskij authored May 28, 2024
1 parent 9bf83df commit 33627ad
Show file tree
Hide file tree
Showing 15 changed files with 45,695 additions and 3 deletions.
11 changes: 11 additions & 0 deletions dist/channel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42999,6 +42999,17 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
});
}
getBases(charm, targetTrack) {
return __awaiter(this, void 0, void 0, function* () {
// Get status of this charm as a structured object
const charmcraftStatus = yield this.statusJson(charm);
const trackIndex = charmcraftStatus.findIndex((track) => track.track === targetTrack);
if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}
return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
});
}
release(charm, charmRevision, destinationChannel, resourceInfo) {
return __awaiter(this, void 0, void 0, function* () {
const resourceArgs = [];
Expand Down
11 changes: 11 additions & 0 deletions dist/check-libraries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43194,6 +43194,17 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
});
}
getBases(charm, targetTrack) {
return __awaiter(this, void 0, void 0, function* () {
// Get status of this charm as a structured object
const charmcraftStatus = yield this.statusJson(charm);
const trackIndex = charmcraftStatus.findIndex((track) => track.track === targetTrack);
if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}
return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
});
}
release(charm, charmRevision, destinationChannel, resourceInfo) {
return __awaiter(this, void 0, void 0, function* () {
const resourceArgs = [];
Expand Down
45,423 changes: 45,423 additions & 0 deletions dist/promote-charm/index.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions dist/release-charm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43090,6 +43090,17 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
});
}
getBases(charm, targetTrack) {
return __awaiter(this, void 0, void 0, function* () {
// Get status of this charm as a structured object
const charmcraftStatus = yield this.statusJson(charm);
const trackIndex = charmcraftStatus.findIndex((track) => track.track === targetTrack);
if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}
return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
});
}
release(charm, charmRevision, destinationChannel, resourceInfo) {
return __awaiter(this, void 0, void 0, function* () {
const resourceArgs = [];
Expand Down
11 changes: 11 additions & 0 deletions dist/release-libraries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43221,6 +43221,17 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
});
}
getBases(charm, targetTrack) {
return __awaiter(this, void 0, void 0, function* () {
// Get status of this charm as a structured object
const charmcraftStatus = yield this.statusJson(charm);
const trackIndex = charmcraftStatus.findIndex((track) => track.track === targetTrack);
if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}
return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
});
}
release(charm, charmRevision, destinationChannel, resourceInfo) {
return __awaiter(this, void 0, void 0, function* () {
const resourceArgs = [];
Expand Down
11 changes: 11 additions & 0 deletions dist/upload-bundle/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43069,6 +43069,17 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
});
}
getBases(charm, targetTrack) {
return __awaiter(this, void 0, void 0, function* () {
// Get status of this charm as a structured object
const charmcraftStatus = yield this.statusJson(charm);
const trackIndex = charmcraftStatus.findIndex((track) => track.track === targetTrack);
if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}
return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
});
}
release(charm, charmRevision, destinationChannel, resourceInfo) {
return __awaiter(this, void 0, void 0, function* () {
const resourceArgs = [];
Expand Down
11 changes: 11 additions & 0 deletions dist/upload-charm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43109,6 +43109,17 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
});
}
getBases(charm, targetTrack) {
return __awaiter(this, void 0, void 0, function* () {
// Get status of this charm as a structured object
const charmcraftStatus = yield this.statusJson(charm);
const trackIndex = charmcraftStatus.findIndex((track) => track.track === targetTrack);
if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}
return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
});
}
release(charm, charmRevision, destinationChannel, resourceInfo) {
return __awaiter(this, void 0, void 0, function* () {
const resourceArgs = [];
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
"author": "Kenneth Koski <[email protected]>",
"license": "GPL-3.0-only",
"scripts": {
"build": "npm run build:upload-charm && npm run build:upload-bundle && npm run build:channel && npm run build:check-libraries && npm run build:release-charm && npm run build:release-libraries",
"build": "npm run build:upload-charm && npm run build:upload-bundle && npm run build:channel && npm run build:check-libraries && npm run build:promote-charm && npm run build:release-charm && npm run build:release-libraries",
"build:upload-bundle": "ncc build src/entries/upload-bundle.ts -o dist/upload-bundle",
"build:upload-charm": "ncc build src/entries/upload-charm.ts -o dist/upload-charm",
"build:channel": "ncc build src/entries/channel.ts -o dist/channel -d",
"build:check-libraries": "ncc build src/entries/check-libraries.ts -o dist/check-libraries",
"build:promote-charm": "ncc build src/entries/promote-charm.ts -o dist/promote-charm",
"build:release-charm": "ncc build src/entries/release-charm.ts -o dist/release-charm",
"build:release-libraries": "ncc build src/entries/release-libraries.ts -o dist/release-libraries",
"test": "jest",
Expand Down
61 changes: 61 additions & 0 deletions promote-charm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# canonical/charming-actions/promote-charm

This action is used to promote an already uploaded charm to a different channel in charmhub. It is designed to be manually triggered with the inputs. The promotion involves all the existing bases for the charm.
## Usage

```yaml
name: Promote charm from a specific track and channel

on:
workflow_dispatch:
inputs:
destination-channel:
description: 'Destination Channel'
required: true
origin-channel:
description: 'Origin Channel'
required: true

# for multi charm repo
charm-name:
description: 'Charm Name'
required: true
jobs:
promote-charm:
name: Promote charm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Release charm to channel
uses: canonical/charming-actions/promote-charm
with:
credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }}
github-token: ${{ secrets.GITHUB_TOKEN }}
destination-channel: ${{ github.event.inputs.destination-channel }}
origin-channel: ${{ github.event.inputs.origin-channel }}

# for multi charm repo
charm-path: charms/${{ github.event.inputs.charm-name }}
```
In multi charm repo, you would also need to provide the charm path; this is necessary to correctly extract the charm metadata for the specific charm you are releasing. By convention, it should be the same the name of the charm. The example yaml provided above should work for must multi charm repo setup.
## API
### Inputs
| Key | Description | Required |
| -------------------- | ------------------------------------------------------------------------------------------------------- | -------- |
| `credentials` | Credentials [exported](https://juju.is/docs/sdk/remote-env-auth) using `charmcraft login --export`. | ✔️ |
| `destination-channel`| Channel to which the charm will be released. It must be in the format of `track/risk`. | ✔️ |
| `origin-channel` | Origin Channel from where the charm that needs to be promoted will be pulled. | ✔️ |
| `charm-path` | Path to the charm where `metadata.yaml` is located. Defaults to the current working directory. | |
| `charmcraft-channel` | Snap channel to use when installing charmcraft. Defaults to `latest/edge`. | |

### Outputs

None

### Limitations
- The origin and destination channels must be in the format of `track/risk` for parsing the charmcraft status output.
- Only works for charm. It does not support releasing bundles.
34 changes: 34 additions & 0 deletions promote-charm/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Charmhub Promotion
description: Promote a charm from a channel to another.
author: Luca Bello
inputs:
destination-channel:
required: true
description: |
Channel to which the charm will be released
origin-channel:
required: true
description: |
Origin Channel from where the charm that needs to be promoted will be
pulled.
charm-path:
required: false
default: '.'
description: |
Path to charm directory
charmcraft-channel:
required: false
default: 'latest/edge'
description: |
Snap channel to use when installing charmcraft
credentials:
required: true
description: |
Credentials exported from `charmcraft login --export`. See
https://juju.is/docs/sdk/remote-env-auth for more info
runs:
using: node20
main: ../dist/release-charm/index.js
branding:
icon: upload-cloud
color: orange
1 change: 1 addition & 0 deletions src/actions/promote-charm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './promote-charm';
80 changes: 80 additions & 0 deletions src/actions/promote-charm/promote-charm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as core from '@actions/core';

import { Snap, Charmcraft, Artifact } from '../../services';
import { Base } from '../../services/charmcraft/types';
import { RevisionResourceInfo } from '../../types';

export class PromoteCharmAction {
private artifacts: Artifact;
private snap: Snap;
private charmcraft: Charmcraft;

private destinationChannel: string;
private originChannel: string;

private charmcraftChannel: string;
private charmPath: string;

constructor() {
this.destinationChannel = core.getInput('destination-channel');
this.originChannel = core.getInput('origin-channel');
this.charmcraftChannel = core.getInput('charmcraft-channel');
this.charmPath = core.getInput('charm-path');

this.artifacts = new Artifact();
this.snap = new Snap();
this.charmcraft = new Charmcraft();
}

async getRevisions(
name: string,
track: string,
channel: string,
bases: Base[],
): Promise<RevisionResourceInfo[]> {
return Promise.all(
bases.map(async (base: Base) =>
this.charmcraft.getRevisionInfoFromChannelJson(
name,
track,
channel,
base,
),
),
);
}

async run() {
try {
await this.snap.install('charmcraft', this.charmcraftChannel);
process.chdir(this.charmPath!);
const { name: charmName } = this.charmcraft.metadata();

const [originTrack, originChannel] = this.originChannel.split('/');

const basesArray = await this.charmcraft.getBases(charmName, originTrack);
const revisions = await this.getRevisions(
charmName,
originTrack,
originChannel,
basesArray,
);
await Promise.all(
revisions.map(async ({ charmRev, resources }) =>
this.charmcraft.release(
charmName,
charmRev,
this.destinationChannel,
resources,
),
),
);
} catch (error: any) {
core.setFailed(error.message);
core.error(error.stack);
}

const result = await this.artifacts.uploadLogs();
core.info(result);
}
}
6 changes: 6 additions & 0 deletions src/entries/promote-charm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PromoteCharmAction } from '../actions/promote-charm/promote-charm';

// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
await new PromoteCharmAction().run();
})();
19 changes: 17 additions & 2 deletions src/services/charmcraft/charmcraft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as glob from '@actions/glob';
import * as fs from 'fs';
import * as yaml from 'js-yaml';

import { Metadata, ResourceInfo } from '../../types';
import { Metadata, ResourceInfo, RevisionResourceInfo } from '../../types';
import { getImageDigest } from '../docker';
import { Base, Mapping, Status, Track } from './types';

Expand Down Expand Up @@ -315,7 +315,7 @@ class Charmcraft {
targetTrack: string,
targetChannel: string,
targetBase: Base,
): Promise<{ charmRev: string; resources: Array<ResourceInfo> }> {
): Promise<RevisionResourceInfo> {
const acceptedChannels = ['stable', 'candidate', 'beta', 'edge'];
if (!acceptedChannels.includes(targetChannel)) {
throw new Error(
Expand Down Expand Up @@ -385,6 +385,21 @@ class Charmcraft {
return { charmRev: revision.toString(), resources: resourceInfoArray };
}

async getBases(charm: string, targetTrack: string): Promise<Base[]> {
// Get status of this charm as a structured object
const charmcraftStatus = await this.statusJson(charm);

const trackIndex = charmcraftStatus.findIndex(
(track: Track) => track.track === targetTrack,
);

if (trackIndex === -1) {
throw new Error(`No track with name ${targetTrack}`);
}

return charmcraftStatus[trackIndex].mappings.map((x) => x.base);
}

async release(
charm: string,
charmRevision: string,
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export interface ResourceInfo {
resourceRev: string;
}

export interface RevisionResourceInfo {
charmRev: string;
resources: ResourceInfo[];
}

export interface Version {
major: number;
minor: number;
Expand Down

0 comments on commit 33627ad

Please sign in to comment.