diff --git a/.github/DISCUSSION_TEMPLATE/discussions.yml b/.github/DISCUSSION_TEMPLATE/discussions.yml index a201e2f9..b51f6f89 100644 --- a/.github/DISCUSSION_TEMPLATE/discussions.yml +++ b/.github/DISCUSSION_TEMPLATE/discussions.yml @@ -29,7 +29,6 @@ body: label: Relevant Logs description: | Please copy and paste any relevant log output. - > [!TIP] > This will be automatically formatted into code, there is no need to insert backticks. render: shell - type: textarea @@ -38,7 +37,6 @@ body: label: Anything Else description: | Links? References? Anything that will provide more context. - > [!TIP] > You can attach images or log files by clicking this area to highlight it and then dragging files in. - type: checkboxes id: terms diff --git a/.github/actions/config/production.js b/.github/actions/config/production.js index 09236122..28a97d0e 100644 --- a/.github/actions/config/production.js +++ b/.github/actions/config/production.js @@ -11,27 +11,27 @@ module.exports = { */ issue: { /** - * Controls automatic label creation for repository + * Controls automatic label update for repository * - * When enabled, the system will automatically create missing labels defined + * When enabled, the system will automatically update repository labels defined * in the labels configuration during workflow execution. When disabled, - * the system will only use existing labels without creating missing ones. + * the system will only use existing labels. * - * Note: If enabled, the system will create GitHub issues during each workflow run, + * Note: If enabled, the system will update GitHub issues during each workflow run, * including a reminder to set this value to "false" after initial setup. This can result in * numerous notifications for repository maintainers. * * @type {boolean} * @default false */ - createLabels: false, + updateLabels: false, /** * Predefined issue label definitions used across the repository * * Contains standardized label definitions (color, description) that are used * for categorizing issues in GitHub and generating release notes. These labels - * can be automatically created when createLabels is true. + * can be automatically updated when updateLabels is true. * * @type {Object} */ diff --git a/.github/actions/core/Logger.js b/.github/actions/core/Logger.js index e4feb1b0..46294cd2 100644 --- a/.github/actions/core/Logger.js +++ b/.github/actions/core/Logger.js @@ -55,7 +55,7 @@ class Logger { */ #format(message, meta = {}) { const parts = [`[${this.context}]`]; - if (meta.level && meta.level !== 'info') parts.push(`[${meta.level.toUpperCase()}]`); + if (meta.level) parts.push(`[${meta.level.toUpperCase()}]`); if (meta.timestamp) parts.push(`[${new Date().toISOString()}]`); if (meta.component) parts.push(`[${meta.component}]`); return `${parts.join('')} ${message}`; @@ -71,7 +71,19 @@ class Logger { debug(message, meta = {}) { if (!this.#allow('debug')) return; const logMeta = { level: 'debug', timestamp: true, ...meta }; - this.core.info(this.#format(message, logMeta)); + const formattedMessage = this.#format(message, logMeta); + if (meta.file) { + const params = { + file: meta.file, + startLine: meta.line || 1, + startColumn: meta.col || 1, + title: meta.title || 'Debug', + message: formattedMessage + }; + this.core.info(formattedMessage, params); + } else { + this.core.info(formattedMessage); + } } /** @@ -109,7 +121,19 @@ class Logger { info(message, meta = {}) { if (!this.#allow('info')) return; const logMeta = { level: 'info', ...meta }; - this.core.info(this.#format(message, logMeta)); + const formattedMessage = this.#format(message, logMeta); + if (meta.file) { + const params = { + file: meta.file, + startLine: meta.line || 1, + startColumn: meta.col || 1, + title: meta.title || 'Info', + message: formattedMessage + }; + this.core.info(formattedMessage, params); + } else { + this.core.info(formattedMessage); + } } /** diff --git a/.github/actions/handlers/Workflow.js b/.github/actions/handlers/Workflow.js index c998a462..791b5346 100644 --- a/.github/actions/handlers/Workflow.js +++ b/.github/actions/handlers/Workflow.js @@ -54,8 +54,8 @@ class WorkflowHandler extends Action { return this.execute('configure repository', async () => { this.logger.info('Configuring repository for workflow operations...'); await this.gitService.configure(); - this.core.setOutput('publish', this.publish()); this.logger.info('Repository configuration complete'); + this.core.setOutput('publish', this.publish()); }); } @@ -92,14 +92,10 @@ class WorkflowHandler extends Action { async reportIssue() { return this.execute('report workflow issue', async () => { this.logger.info('Checking for workflow issues...'); - if (this.config.get('issue.createLabels') === true && this.context.workflow === 'Chart') { - this.logger.warning('Set "createLabels: false" in config.js after initial setup, to optimize workflow performance.'); - } const templatePath = this.config.get('workflow.template'); const templateContent = await this.fileService.read(templatePath); const issue = await this.issueService.report( this.context, - this.labelService, { content: templateContent, service: this.templateService @@ -132,24 +128,12 @@ class WorkflowHandler extends Action { */ async updateCharts() { return this.execute('update charts', async () => { + if (this.config.get('issue.updateLabels')) await this.labelService.update(); this.logger.info('Starting the charts update process...'); await this.chartHandler.process(); - this.logger.info('Successfully completed the charts update process'); + this.logger.info('Charts update process complete'); }); } - - /** - * Update issue labels - * - * @returns {Promise} - */ - async updateLabels() { - return this.execute('update issue labels', async () => { - this.logger.info('Updating repository issue labels...'); - await this.labelService.update(); - this.logger.info('Repository issue labels update complete'); - }, false); - } } module.exports = WorkflowHandler; diff --git a/.github/actions/services/Issue.js b/.github/actions/services/Issue.js index 775d2921..58618d1d 100644 --- a/.github/actions/services/Issue.js +++ b/.github/actions/services/Issue.js @@ -6,8 +6,7 @@ * @license BSD-3-Clause */ const Action = require('../core/Action'); -const GraphQLService = require('./github/GraphQL'); -const RestService = require('./github/Rest'); +const GitHubService = require('./github'); /** * Issue service for GitHub issue and label operations @@ -25,42 +24,8 @@ class IssueService extends Action { */ constructor(params) { super(params); - this.graphqlService = new GraphQLService(params); - this.restService = new RestService(params); - } - - /** - * Validates if workflow has issues that warrant creating an issue - * - * @private - * @param {number} id - Workflow run ID - * @returns {Promise} True if issues detected - */ - async #validate(id) { - return this.execute('validate workflow status', async () => { - let hasFailures = false; - const workflowRun = await this.restService.getWorkflowRun(id); - if (['cancelled', 'failure'].includes(workflowRun.conclusion)) { - return true; - } - const jobs = await this.restService.listJobs(); - for (const job of jobs) { - if (job.steps) { - const failedSteps = job.steps.filter(step => step.conclusion !== 'success'); - if (failedSteps.length) { - hasFailures = true; - break; - } - } - } - const hasWarnings = await this.execute('validate workflow warnings', async () => { - const logsData = await this.restService.getWorkflowRunLogs(id); - if (!logsData) return false; - const regex = /(^|:)warning:/i; - return regex.test(logsData); - }, false); - return hasFailures || hasWarnings; - }, false); + this.graphqlService = new GitHubService.GraphQL(params); + this.restService = new GitHubService.Rest(params); } /** @@ -107,17 +72,16 @@ class IssueService extends Action { * Prepares and creates a workflow issue * * @param {Object} context - GitHub Actions context - * @param {Object} label - Label service instance * @param {Object} [template={}] - Template configuration * @param {string} template.content - Issue template content * @param {Object} template.service - Template service instance * @returns {Promise} Created issue data or null on failure */ - async report(context, label, template = {}) { + async report(context, template = {}) { return this.execute('report workflow issue', async () => { const { content, service } = template; - const hasIssues = await this.#validate(context.runId); - if (!hasIssues) return null; + const annotations = await this.restService.getAnnotations(); + if (!annotations.length) return null; const repoUrl = context.payload.repository.html_url; const isPullRequest = Boolean(context.payload.pull_request); const branchName = isPullRequest @@ -134,14 +98,10 @@ class IssueService extends Action { RepoURL: repoUrl }); if (!issueBody) return null; - const labelNames = this.config.get('workflow.labels'); - if (this.config.get('issue.createLabels') && label) { - await Promise.all(labelNames.map(labelName => label.add(labelName))); - } return this.restService.createIssue( this.config.get('workflow.title'), issueBody, - labelNames + this.config.get('workflow.labels') ); }, false); } diff --git a/.github/actions/services/Label.js b/.github/actions/services/Label.js index 2450b242..80af5fd1 100644 --- a/.github/actions/services/Label.js +++ b/.github/actions/services/Label.js @@ -28,34 +28,30 @@ class LabelService extends Action { } /** - * Adds a label to the repository if it doesn't exist + * Sets a label to the repository * - * @param {string} name - Name of the label to add - * @returns {Promise} True if label was created or exists + * @private + * @param {string} name - Name of the label to set + * @returns {Promise} True if label was created or updated */ - async add(name) { - return this.execute(`add '${name}' label`, async () => { + async #set(name) { + return this.execute(`set '${name}' label`, async () => { if (!name) { this.logger.warning('Label name is required'); return false; } const labelConfig = this.config.get(`issue.labels.${name}`); if (!labelConfig) { - this.logger.warning(`Label configuration not found for '${name}'`); + this.logger.warning(`Label '${name}' configuration not found`); return false; } const existingLabel = await this.restService.getLabel(name); - if (existingLabel) return true; - if (!this.config.get('issue.createLabels')) { - this.logger.warning(`Label '${name}' not found and createLabels is disabled`); - return false; - } - await this.restService.createLabel( + const method = existingLabel ? 'updateLabel' : 'createLabel'; + await this.restService[method]( name, labelConfig.color, labelConfig.description ); - this.logger.info(`Successfully created '${name}' label`); return true; }, false); } @@ -63,27 +59,21 @@ class LabelService extends Action { /** * Update repository labels based on configuration * - * @returns {Promise} Array of created label names + * @returns {Promise} */ async update() { return this.execute('update repository issue labels', async () => { - if (!this.config.get('issue.createLabels')) { - this.logger.info('Label creation is disabled in configuration, skipping label updates'); - return []; - } this.logger.info('Updating repository issue labels...'); + const message = 'Set "updateLabels: false" after initial setup'; + await this.restService.createAnnotation(message, { level: 'warning' }); + this.logger.warning(message); const labelNames = Object.keys(this.config.get('issue.labels')); - const results = await Promise.all( + await Promise.all( labelNames.map(async labelName => { - const created = await this.add(labelName); - return created ? labelName : null; + await this.#set(labelName); }) ); - const createdLabels = results.filter(Boolean); - if (createdLabels.length) { - this.logger.info(`Successfully updated ${createdLabels.length} issue labels`); - } - return createdLabels; + this.logger.info(`Successfully updated ${labelNames.length} issue labels`); }, false); } } diff --git a/.github/actions/services/github/Rest.js b/.github/actions/services/github/Rest.js index 1e3cbb34..09c50d94 100644 --- a/.github/actions/services/github/Rest.js +++ b/.github/actions/services/github/Rest.js @@ -87,6 +87,42 @@ class RestService extends ApiService { return result; } + /** + * Creates an annotation for the current workflow run + * + * @param {string} message - Annotation message + * @param {Object} [options={}] - Annotation options + * @param {Object} [options.level] - Annotation level (notice, warning, error) + * @param {Object} [options.status] - Annotation status + * @param {Object} [options.conclusion] - Annotation conclusion + * @returns {Promise} Created check run data + */ + async createAnnotation(message, options = {}) { + return this.execute('create annotation', async () => { + const { level = 'notice', status = 'completed', conclusion = 'neutral' } = options; + const checkRun = await this.github.rest.checks.create({ + owner: this.context.repo.owner, + repo: this.context.repo.repo, + name: level, + head_sha: this.context.sha, + status, + conclusion, + output: { + title: level, + summary: message, + annotations: [{ + path: level, + start_line: 1, + end_line: 1, + annotation_level: level, + message + }] + } + }); + return checkRun.data; + }); + } + /** * Creates a GitHub issue * @@ -233,6 +269,40 @@ class RestService extends ApiService { }, false); } + /** + * Gets annotations for the current workflow run + * + * @returns {Promise>} Array of annotations + */ + async getAnnotations() { + return this.execute('get annotations', async () => { + const checkRuns = await this.github.rest.checks.listForRef({ + owner: this.context.repo.owner, + repo: this.context.repo.repo, + ref: this.context.sha + }); + const annotations = []; + for (const checkRun of checkRuns.data.check_runs) { + if (checkRun.output?.annotations_url) { + try { + const response = await this.github.request(checkRun.output.annotations_url); + if (response.data && response.data.length > 0) { + annotations.push(...response.data.map(annotation => ({ + level: annotation.annotation_level, + message: annotation.message, + path: annotation.path, + line: annotation.start_line + }))); + } + } catch (error) { + continue; + } + } + } + return annotations; + }, false); + } + /** * Gets a label from a repository * @@ -325,63 +395,6 @@ class RestService extends ApiService { }, false); } - /** - * Gets workflow run data - * - * @param {number} id - Workflow run ID - * @returns {Promise} Workflow run data - */ - async getWorkflowRun(id) { - return this.execute(`get workflow run '${id}' ID`, async () => { - const response = await this.github.rest.actions.getWorkflowRun({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - run_id: id - }); - return { - id: response.data.id, - status: response.data.status, - conclusion: response.data.conclusion, - url: response.data.html_url, - createdAt: response.data.created_at, - updatedAt: response.data.updated_at - }; - }, false); - } - - /** - * Gets workflow run logs - * - * @param {number} id - Workflow run ID - * @returns {Promise} Workflow run logs data - */ - async getWorkflowRunLogs(id) { - return this.execute(`get workflow run '${id}' logs`, async () => { - const response = await this.github.rest.actions.downloadWorkflowRunLogs({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - run_id: parseInt(id, 10) - }); - return response.data; - }, false, true); - } - - /** - * Lists jobs for a workflow run - * - * @returns {Promise>} Array of job objects with steps - */ - async listJobs() { - return this.execute('list jobs', async () => { - const response = await this.github.rest.actions.listJobsForWorkflowRun({ - owner: this.context.repo.owner, - repo: this.context.repo.repo, - run_id: this.context.runId - }); - return response?.data?.jobs || []; - }, false, true); - } - /** * Uploads an asset to a release * @@ -410,6 +423,33 @@ class RestService extends ApiService { }); } + /** + * Updates a label in a repository + * + * @param {string} name - Label name + * @param {string} color - Label color (hex without #) + * @param {string} description - Label description + * @returns {Promise} Updated label + */ + async updateLabel(name, color, description) { + return this.execute(`update '${name}' label`, async () => { + const response = await this.github.rest.issues.updateLabel({ + owner: this.context.repo.owner, + repo: this.context.repo.repo, + name, + color, + description + }); + this.logger.info(`Successfully updated '${name}' label`); + return { + id: response.data.id, + name: response.data.name, + color: response.data.color, + description: response.data.description + }; + }, false, true); + } + /** * Validates context payload for specific event types * diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1d2107ef..b32c391f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,6 @@ ## Objective - + ## Scope diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index aa409d14..73a84c21 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -56,14 +56,6 @@ jobs: const workflow = new Workflow({ github, context, core, exec }); await workflow.configureRepository(); - - name: Update repository issue labels - uses: actions/github-script@v7 - with: - script: | - const { Workflow } = require('./.github/actions/handlers'); - const workflow = new Workflow({ github, context, core, exec }); - await workflow.updateLabels(); - - name: Install helm-docs uses: actions/github-script@v7 with: diff --git a/application/inventory.yaml b/application/inventory.yaml index 03304067..d79a43a3 100644 --- a/application/inventory.yaml +++ b/application/inventory.yaml @@ -2,7 +2,7 @@ application: - name: sealed-secrets description: Kubernetes secrets controller used for secure Git storage status: modified - version: 1.0.6 + version: 1.0.7 - name: ubuntu description: Ubuntu Server LTS troubleshooting container for Kubernetes cluster status: modified diff --git a/application/sealed-secrets/Chart.yaml b/application/sealed-secrets/Chart.yaml index bfd01a35..dd696a75 100644 --- a/application/sealed-secrets/Chart.yaml +++ b/application/sealed-secrets/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: sealed-secrets description: Kubernetes secrets controller used for secure Git storage -version: 1.0.6 +version: 1.0.7 kubeVersion: ">=1.16.0-0" type: application keywords: diff --git a/application/sealed-secrets/README.md b/application/sealed-secrets/README.md index d55aeac6..32e2f251 100644 --- a/application/sealed-secrets/README.md +++ b/application/sealed-secrets/README.md @@ -2,7 +2,7 @@ sealed-secrets -![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 1.0.6](https://img.shields.io/badge/Version-1.0.6-informational?style=flat-square) ![AppVersion: 2.17.2](https://img.shields.io/badge/AppVersion-2.17.2-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 1.0.7](https://img.shields.io/badge/Version-1.0.7-informational?style=flat-square) ![AppVersion: 2.17.2](https://img.shields.io/badge/AppVersion-2.17.2-informational?style=flat-square) ArgoCD application for the `sealed-secrets` [chart](https://github.com/bitnami-labs/sealed-secrets/blob/helm-v2.17.2/helm/sealed-secrets), deployed into AXIVO [K3s Cluster](https://github.com/axivo/k3s-cluster). Review the cluster [documentation](https://axivo.com/k3s-cluster/), for additional details. The application deployment is also compatible with a generic Kubernetes cluster. @@ -27,7 +27,7 @@ $ kubectl apply -f application/sealed-secrets/application.yaml Alternatively, deploy using Helm directly: ```shell -$ helm install sealed-secrets oci://ghcr.io/axivo/application/sealed-secrets:1.0.6 -n kube-system +$ helm install sealed-secrets oci://ghcr.io/axivo/application/sealed-secrets:1.0.7 -n kube-system ``` ### Chart Dependencies diff --git a/application/sealed-secrets/application.yaml b/application/sealed-secrets/application.yaml index 4b2f1a21..b4e156d7 100644 --- a/application/sealed-secrets/application.yaml +++ b/application/sealed-secrets/application.yaml @@ -14,7 +14,7 @@ spec: - values.yaml path: application/sealed-secrets repoURL: https://github.com/axivo/charts.git - targetRevision: sealed-secrets-1.0.6 + targetRevision: sealed-secrets-1.0.7 syncPolicy: automated: prune: true diff --git a/application/sealed-secrets/metadata.yaml b/application/sealed-secrets/metadata.yaml index fcb4ce1d..3d895abf 100644 --- a/application/sealed-secrets/metadata.yaml +++ b/application/sealed-secrets/metadata.yaml @@ -1,6 +1,40 @@ apiVersion: v1 entries: sealed-secrets: + - annotations: + org.opencontainers.image.documentation: >- + https://github.com/axivo/charts/blob/main/application/sealed-secrets/README.md + org.opencontainers.image.licenses: BSD-3-Clause + apiVersion: v2 + created: '2025-07-21T16:41:31.532348379Z' + dependencies: + - name: common + repository: oci://ghcr.io/axivo/charts/library + version: 1.0.0 + - name: sealed-secrets + repository: https://bitnami-labs.github.io/sealed-secrets + version: 2.17.2 + description: Kubernetes secrets controller used for secure Git storage + digest: f0ed9f9b1cc18ec4fb8e758b380b80210cf40e767d7f4e95cec85ebefb6a5d9c + home: https://github.com/axivo/charts + icon: >- + https://raw.githubusercontent.com/axivo/charts/refs/heads/main/application/sealed-secrets/icon.png + keywords: + - sealed-secrets + - secrets + kubeVersion: '>=1.16.0-0' + maintainers: + - name: AXIVO + url: https://axivo.com + name: sealed-secrets + sources: + - https://github.com/axivo/charts + - https://github.com/bitnami-labs/sealed-secrets + type: application + urls: + - >- + https://github.com/axivo/charts/releases/download/sealed-secrets-1.0.7/application.tgz + version: 1.0.7 - annotations: org.opencontainers.image.documentation: >- https://github.com/axivo/charts/blob/main/application/sealed-secrets/README.md @@ -239,4 +273,4 @@ entries: - >- https://github.com/axivo/charts/releases/download/sealed-secrets-1.0.0/application.tgz version: 1.0.0 -generated: '2025-07-21T11:33:59.217030625Z' +generated: '2025-07-21T16:41:31.529872728Z'