Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ on:
zap-target-port:
description: ZAP target port
type: string
default: 80
default: '80'
zap-target-protocol:
description: ZAP target protocol
type: string
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration_test_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ on:
zap-target-port:
description: ZAP target port
type: string
default: 80
default: '80'
zap-target-protocol:
description: ZAP target protocol
type: string
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ The following secrets are available for this workflow:
When running the integration tests, the following posargs will be automatically passed to the `integration` target:

* --charm-file [charm_file_name]: The name of the charm artifact generated prior to the integration tests run, this argument can be supplied multiple times for charm with multiple bases.
* --{image-name}-image: The name of the image artifact built prior to the integration tests run, this argument may be supplied multiple times or not at all depending on the plan
* --{resource-name}-resource: The name of the charm file resources built prior to the integration tests run, this argument may be supplied multiple times or not at all depending on the plan
* --series [series]: As defined in the `series` configuration described option above
* -k [module]: As defined in the `modules` configuration option described above
* --keep-models
Expand Down
5,314 changes: 2,752 additions & 2,562 deletions dist/build/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/build/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/build/sourcemap-register.js

Large diffs are not rendered by default.

4,782 changes: 2,458 additions & 2,324 deletions dist/plan-integration/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/plan-integration/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/plan-integration/sourcemap-register.js

Large diffs are not rendered by default.

5,967 changes: 3,668 additions & 2,299 deletions dist/plan-scan/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/plan-scan/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/plan-scan/sourcemap-register.js

Large diffs are not rendered by default.

6,515 changes: 3,987 additions & 2,528 deletions dist/plan/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/plan/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/plan/sourcemap-register.js

Large diffs are not rendered by default.

5,115 changes: 2,655 additions & 2,460 deletions dist/publish/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/publish/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/publish/sourcemap-register.js

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,35 @@ interface BuildDockerImageParams {
token: string
}

async function buildFileResource(plan: BuildPlan): Promise<void> {
core.startGroup(`Build resource {plan.name}`)
if (!plan.build_target) {
throw new Error('build_target is required for file resources')
}
await exec.exec(`./${plan.source_file}`, [plan.build_target], {
cwd: plan.source_directory
})
core.endGroup()
const resourceFiles = await (
await glob.create(path.join(plan.source_directory, plan.build_target))
).glob()
const artifact = new DefaultArtifactClient()
const manifestFile = path.join(plan.source_directory, 'manifest.json')
fs.writeFileSync(
manifestFile,
JSON.stringify(
{ name: plan.name, files: resourceFiles.map(f => path.basename(f)) },
null,
2
)
)
await artifact.uploadArtifact(
plan.output,
[...resourceFiles, manifestFile],
plan.source_directory
)
}

async function buildDockerImage({
plan,
user,
Expand Down Expand Up @@ -366,6 +395,10 @@ export async function run(): Promise<void> {
user: github.context.actor,
token: core.getInput('github-token')
})
break
case 'file':
await buildFileResource(plan)
break
}
} catch (error) {
// Fail the workflow run if an error occurs
Expand Down
10 changes: 9 additions & 1 deletion src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ export interface Plan {
}

export interface BuildPlan {
type: 'charm' | 'rock' | 'docker-image'
type: 'charm' | 'rock' | 'docker-image' | 'file'
name: string
source_file: string
source_directory: string
build_target: string | undefined
output_type: 'file' | 'registry'
output: string
}

export interface CharmResource {
type: 'file' | 'oci-image'
description?: string
filename?: string
'upstream-source'?: string
}
14 changes: 9 additions & 5 deletions src/plan-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@ export async function run(): Promise<void> {
const manifest = JSON.parse(
fs.readFileSync(path.join(tmp, 'manifest.json'), { encoding: 'utf-8' })
) as object
if (build.type === 'charm') {
if (build.type === 'charm' || build.type === 'file') {
// @ts-ignore
for (const file of manifest.files) {
for (const file of manifest.files as string[]) {
fs.renameSync(
path.join(tmp, file),
path.join(plan.working_directory, file)
)
args.push(`--charm-file=./${file}`)
const file_path = path.resolve(plan.working_directory, file)
// @ts-ignore
const name = manifest.name as string
let argName: string =
build.type === 'charm' ? 'charm-file' : `${name}-resource`
args.push(`--${argName}=${file_path}`)
}
}
if (build.type === 'rock' || build.type == 'docker-image') {
} else if (build.type === 'rock' || build.type == 'docker-image') {
// @ts-ignore
const name = manifest.name as string
if ('files' in manifest) {
Expand Down
80 changes: 77 additions & 3 deletions src/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as path from 'path'
import * as yaml from 'js-yaml'
import * as fs from 'fs'
import * as github from '@actions/github'
import { Plan, BuildPlan } from './model'
import { Plan, BuildPlan, CharmResource } from './model'
import { DefaultArtifactClient } from '@actions/artifact'
import * as os from 'os'

Expand Down Expand Up @@ -71,6 +71,7 @@ async function planBuildCharm(
name,
source_file: file,
source_directory: path.dirname(file),
build_target: undefined,
output_type: 'file',
output: sanitizeArtifactName(`${id}__build__output__charm__${name}`)
}
Expand All @@ -97,6 +98,7 @@ async function planBuildRock(
name,
source_file: file,
source_directory: path.dirname(file),
build_target: undefined,
output_type: outputType,
output: sanitizeArtifactName(`${id}__build__output__rock__${name}`)
}
Expand All @@ -119,6 +121,7 @@ async function planBuildDockerImage(
name,
source_file: file,
source_directory: path.dirname(file),
build_target: undefined,
output_type: outputType,
output: sanitizeArtifactName(
`${id}__build__output__docker-image__${name}`
Expand All @@ -127,6 +130,76 @@ async function planBuildDockerImage(
})
}

async function planBuildFileResource(
workingDir: string,
id: string
): Promise<BuildPlan[]> {
const allCharmcraftFiles = await (
await glob.create(path.join(workingDir, '**', 'charmcraft.yaml'))
).glob()
const charmcraftFiles = allCharmcraftFiles.filter(
file =>
!path.normalize(path.relative(workingDir, file)).startsWith('tests/')
)
return charmcraftFiles.flatMap((charmcraftFile: string) => {
const file = path.join(
workingDir,
path.relative(workingDir, charmcraftFile)
)
const charmcraft = yaml.load(
fs.readFileSync(charmcraftFile, { encoding: 'utf-8' })
) as object
const metadataFile = path.join(
path.dirname(charmcraftFile),
'metadata.yaml'
)
const metadataExists = fs.existsSync(metadataFile)
const metadata = metadataExists
? (yaml.load(
fs.readFileSync(metadataFile, { encoding: 'utf-8' })
) as object)
: {}

let charmName: string = ''
if ('name' in charmcraft) {
charmName = charmcraft['name'] as string
} else if ('name' in metadata) {
charmName = metadata.name as string
} else {
throw new Error(`unknown charm name (${workingDir})`)
}

let resources: Map<string, CharmResource> = new Map()
if ('resources' in charmcraft) {
resources = charmcraft['resources'] as Map<string, CharmResource>
}
if ('resources' in metadata) {
resources = metadata['resources'] as Map<string, CharmResource>
}

return Object.entries(resources).reduce(
(acc, [resourceName, resource]: [string, CharmResource]) => {
if (resource.type === 'file' && resource.filename) {
let parent = path.dirname(file)
acc.push({
type: 'file',
name: resourceName,
source_file: `build-${resourceName}.sh`,
build_target: resource.filename,
source_directory: parent,
output_type: 'file',
output: sanitizeArtifactName(
`${id}__build__output__file__${charmName}__${resourceName}`
)
})
}
return acc
},
[] as BuildPlan[]
)
})
}

async function planBuild(
workingDir: string,
id: string,
Expand All @@ -135,7 +208,8 @@ async function planBuild(
return [
...(await planBuildCharm(workingDir, id)),
...(await planBuildRock(workingDir, id, imageOutputType)),
...(await planBuildDockerImage(workingDir, id, imageOutputType))
...(await planBuildDockerImage(workingDir, id, imageOutputType)),
...(await planBuildFileResource(workingDir, id))
]
}

Expand Down Expand Up @@ -172,7 +246,7 @@ export async function run(): Promise<void> {
working_directory: workingDir,
build: buildPlans
}
core.info(`generated workflow plan: ${JSON.stringify(plan, null, 2)}`)
core.info(`Generated workflow plan: ${JSON.stringify(plan, null, 2)}`)
const artifact = new DefaultArtifactClient()
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-'))
const pathFile = path.join(tmp, 'plan.json')
Expand Down
99 changes: 88 additions & 11 deletions src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ class Publish {
throw new Error(`can't find plan artifact for workflow run ${runId}`)
}

async getImageResources(): Promise<string[]> {
async getCharmResources(): Promise<[string[], string[]]> {
interface Metadata {
resources?: {
[name: string]: {
type: string
filename?: string
'upstream-source'?: string
}
}
Expand Down Expand Up @@ -185,15 +186,69 @@ class Publish {
}
const resources = metadata.resources
if (resources === undefined) {
return []
return [[], []]
}
return Object.keys(resources).filter(
let images = Object.keys(resources).filter(
k => resources[k].type === 'oci-image' && !resources[k]['upstream-source']
)
let files = Object.keys(resources).filter(k => resources[k].type === 'file')
return [images, files]
}

async getFiles(): Promise<Map<string, string>> {
const [, resources] = await this.getCharmResources()
core.info(`required resources: ${resources}`)
const upload: Map<string, string> = new Map()
if (resources.length === 0) {
return upload
}
const runId = await this.findWorkflowRunId()
const plan = await this.getPlan(runId)
for (const build of plan.build) {
if (build.type === 'file') {
const resourceName = this.resourceMapping[build.name]
if (!resources.includes(resourceName)) {
core.info(`skip uploading file: ${build.name}`)
continue
}
const tmp = this.mkdtemp()
const artifact = await this.artifact.getArtifact(build.output, {
findBy: {
token: this.token,
repositoryOwner: github.context.repo.owner,
repositoryName: github.context.repo.repo,
workflowRunId: runId
}
})
await this.artifact.downloadArtifact(artifact.artifact.id, {
path: tmp,
findBy: {
token: this.token,
repositoryOwner: github.context.repo.owner,
repositoryName: github.context.repo.repo,
workflowRunId: runId
}
})
const manifest = JSON.parse(
fs.readFileSync(path.join(tmp, 'manifest.json'), {
encoding: 'utf-8'
})
)
const files = manifest.files as string[]
if (files.length !== 1) {
throw new Error(
`file resource ${build.name} contain multiple candidates: ${files}`
)
}
const file = files[0]
upload.set(resourceName, file)
}
}
return upload
}

async getImages(): Promise<Map<string, string>> {
const resources = await this.getImageResources()
const [resources] = await this.getCharmResources()
core.info(`required resources: ${resources}`)
const upload: Map<string, string> = new Map()
if (resources.length === 0) {
Expand All @@ -203,7 +258,7 @@ class Publish {
const plan = await this.getPlan(runId)
let dockerLogin = false
for (const build of plan.build) {
if (build.type === 'charm') {
if (build.type === 'charm' || build.type === 'file') {
continue
}
const resourceName = this.resourceMapping.hasOwnProperty(build.name)
Expand Down Expand Up @@ -346,7 +401,8 @@ class Publish {
async run() {
try {
core.startGroup('retrieve image info')
const images = await this.getImages()
const imageResources = await this.getImages()
const fileResources = await this.getFiles()
core.endGroup()
core.startGroup('retrieve charm info')
const {
Expand All @@ -355,15 +411,36 @@ class Publish {
files: charms
} = await this.getCharms()
core.endGroup()
core.info(
`start uploading image resources: ${JSON.stringify(Object.fromEntries([...images]))}`
)
for (const resource of images.keys()) {
if (fileResources.size !== 0) {
core.info(
`start uploading file resources: ${JSON.stringify(Object.fromEntries([...fileResources]))}`
)
}
for (const [resource, filePath] of fileResources) {
core.info(`upload resource ${resource}`)
await exec.exec(
'charmcraft',
[
'upload-resource',
charmName,
resource,
`--filepath=${filePath}`,
'--verbosity=brief'
],
{ env: { CHARMCRAFT_AUTH: this.charmhubToken } }
)
}
if (imageResources.size !== 0) {
core.info(
`start uploading image resources: ${JSON.stringify(Object.fromEntries([...imageResources]))}`
)
}
for (const [resource, image] of imageResources) {
core.info(`upload resource ${resource}`)
const imageId = (
await exec.getExecOutput('docker', [
'images',
images.get(resource) as string,
image,
'--format',
'{{.ID}}'
])
Expand Down
Loading
Loading