mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-23 22:56:18 +00:00
Merge branch 'origin/main' into 'next-release/main'
This commit is contained in:
commit
b1214e930d
6 changed files with 144 additions and 1 deletions
|
@ -2012,6 +2012,53 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{repo_id}/logs/{number}/{stepId}": {
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"text/plain"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Pipeline logs"
|
||||||
|
],
|
||||||
|
"summary": "Deletes step log",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "Bearer \u003cpersonal access token\u003e",
|
||||||
|
"description": "Insert your personal access token",
|
||||||
|
"name": "Authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the repository id",
|
||||||
|
"name": "repo_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the number of the pipeline",
|
||||||
|
"name": "number",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "the step id",
|
||||||
|
"name": "stepId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{repo_id}/move": {
|
"/repos/{repo_id}/move": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
|
@ -224,6 +224,66 @@ func GetStepLogs(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, logs)
|
c.JSON(http.StatusOK, logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteStepLogs
|
||||||
|
//
|
||||||
|
// @Summary Deletes step log
|
||||||
|
// @Router /repos/{repo_id}/logs/{number}/{stepId} [delete]
|
||||||
|
// @Produce plain
|
||||||
|
// @Success 204
|
||||||
|
// @Tags Pipeline logs
|
||||||
|
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||||
|
// @Param repo_id path int true "the repository id"
|
||||||
|
// @Param number path int true "the number of the pipeline"
|
||||||
|
// @Param stepId path int true "the step id"
|
||||||
|
func DeleteStepLogs(c *gin.Context) {
|
||||||
|
_store := store.FromContext(c)
|
||||||
|
repo := session.Repo(c)
|
||||||
|
|
||||||
|
pipelineNumber, err := strconv.ParseInt(c.Params.ByName("number"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_pipeline, err := _store.GetPipelineNumber(repo, pipelineNumber)
|
||||||
|
if err != nil {
|
||||||
|
handleDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stepID, err := strconv.ParseInt(c.Params.ByName("stepId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_step, err := _store.StepLoad(stepID)
|
||||||
|
if err != nil {
|
||||||
|
handleDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _step.PipelineID != _pipeline.ID {
|
||||||
|
// make sure we cannot read arbitrary logs by id
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("step with id %d is not part of repo %s", stepID, repo.FullName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch _step.State {
|
||||||
|
case model.StatusRunning, model.StatusPending:
|
||||||
|
c.String(http.StatusUnprocessableEntity, "Cannot delete logs for a pending or running step")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _store.LogDelete(_step)
|
||||||
|
if err != nil {
|
||||||
|
handleDBError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
// GetPipelineConfig
|
// GetPipelineConfig
|
||||||
//
|
//
|
||||||
// @Summary Pipeline configuration
|
// @Summary Pipeline configuration
|
||||||
|
|
|
@ -104,6 +104,7 @@ func apiRoutes(e *gin.RouterGroup) {
|
||||||
repo.POST("/pipelines/:number/decline", session.MustPush, api.PostDecline)
|
repo.POST("/pipelines/:number/decline", session.MustPush, api.PostDecline)
|
||||||
|
|
||||||
repo.GET("/logs/:number/:stepId", api.GetStepLogs)
|
repo.GET("/logs/:number/:stepId", api.GetStepLogs)
|
||||||
|
repo.DELETE("/logs/:number/:stepId", session.MustPush, api.DeleteStepLogs)
|
||||||
|
|
||||||
// requires push permissions
|
// requires push permissions
|
||||||
repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs)
|
repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs)
|
||||||
|
|
|
@ -245,6 +245,8 @@
|
||||||
"pipeline": "Pipeline #{pipelineId}",
|
"pipeline": "Pipeline #{pipelineId}",
|
||||||
"log_title": "Step Logs",
|
"log_title": "Step Logs",
|
||||||
"log_download_error": "There was an error while downloading the log file",
|
"log_download_error": "There was an error while downloading the log file",
|
||||||
|
"log_delete_confirm": "Do you really want to delete logs of this step?",
|
||||||
|
"log_delete_error": "There was an error while deleting the step logs",
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
|
@ -253,6 +255,7 @@
|
||||||
"deploy": "Deploy",
|
"deploy": "Deploy",
|
||||||
"restart_success": "Pipeline restarted",
|
"restart_success": "Pipeline restarted",
|
||||||
"log_download": "Download",
|
"log_download": "Download",
|
||||||
|
"log_delete": "Delete",
|
||||||
"log_auto_scroll": "Automatically scroll down",
|
"log_auto_scroll": "Automatically scroll down",
|
||||||
"log_auto_scroll_off": "Turn off automatic scrolling"
|
"log_auto_scroll_off": "Turn off automatic scrolling"
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,13 @@
|
||||||
icon="download"
|
icon="download"
|
||||||
@click="download"
|
@click="download"
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
v-if="step?.end_time !== undefined && hasLogs && hasPushPermission"
|
||||||
|
:title="$t('repo.pipeline.actions.log_delete')"
|
||||||
|
class="!hover:bg-white !hover:bg-opacity-10"
|
||||||
|
icon="trash"
|
||||||
|
@click="deleteLogs"
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="step?.end_time === undefined"
|
v-if="step?.end_time === undefined"
|
||||||
:title="
|
:title="
|
||||||
|
@ -111,7 +118,7 @@ import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
|
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
|
||||||
import useApiClient from '~/compositions/useApiClient';
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
import useNotifications from '~/compositions/useNotifications';
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
import { Pipeline, Repo } from '~/lib/api/types';
|
import { Pipeline, Repo, RepoPermissions } from '~/lib/api/types';
|
||||||
import { findStep, isStepFinished, isStepRunning } from '~/utils/helpers';
|
import { findStep, isStepFinished, isStepRunning } from '~/utils/helpers';
|
||||||
|
|
||||||
type LogLine = {
|
type LogLine = {
|
||||||
|
@ -136,6 +143,7 @@ const i18n = useI18n();
|
||||||
const pipeline = toRef(props, 'pipeline');
|
const pipeline = toRef(props, 'pipeline');
|
||||||
const stepId = toRef(props, 'stepId');
|
const stepId = toRef(props, 'stepId');
|
||||||
const repo = inject<Ref<Repo>>('repo');
|
const repo = inject<Ref<Repo>>('repo');
|
||||||
|
const repoPermissions = inject<Ref<RepoPermissions>>('repo-permissions');
|
||||||
const apiClient = useApiClient();
|
const apiClient = useApiClient();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
@ -160,6 +168,7 @@ ansiUp.value.use_classes = true;
|
||||||
const logBuffer = ref<LogLine[]>([]);
|
const logBuffer = ref<LogLine[]>([]);
|
||||||
|
|
||||||
const maxLineCount = 5000; // TODO(2653): set back to 500 and implement lazy-loading support
|
const maxLineCount = 5000; // TODO(2653): set back to 500 and implement lazy-loading support
|
||||||
|
const hasPushPermission = computed(() => repoPermissions?.value?.push);
|
||||||
|
|
||||||
function isSelected(line: LogLine): boolean {
|
function isSelected(line: LogLine): boolean {
|
||||||
return route.hash === `#L${line.number}`;
|
return route.hash === `#L${line.number}`;
|
||||||
|
@ -297,6 +306,25 @@ async function loadLogs() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteLogs() {
|
||||||
|
if (!repo?.value || !pipeline.value || !step.value) {
|
||||||
|
throw new Error('The repository, pipeline or step was undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use proper dialog (copy-pasted from web/src/components/secrets/SecretList.vue:deleteSecret)
|
||||||
|
// eslint-disable-next-line no-alert, no-restricted-globals
|
||||||
|
if (!confirm(i18n.t('repo.pipeline.log_delete_confirm'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiClient.deleteLogs(repo.value.id, pipeline.value.number, step.value.id);
|
||||||
|
log.value = [];
|
||||||
|
} catch (e) {
|
||||||
|
notifications.notifyError(e, i18n.t('repo.pipeline.log_delete_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadLogs();
|
await loadLogs();
|
||||||
});
|
});
|
||||||
|
|
|
@ -136,6 +136,10 @@ export default class WoodpeckerClient extends ApiClient {
|
||||||
return this._get(`/api/repos/${repoId}/logs/${pipeline}/${step}`) as Promise<PipelineLog[]>;
|
return this._get(`/api/repos/${repoId}/logs/${pipeline}/${step}`) as Promise<PipelineLog[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteLogs(repoId: number, pipeline: number, step: number): Promise<unknown> {
|
||||||
|
return this._delete(`/api/repos/${repoId}/logs/${pipeline}/${step}`);
|
||||||
|
}
|
||||||
|
|
||||||
getSecretList(repoId: number, page: number): Promise<Secret[] | null> {
|
getSecretList(repoId: number, page: number): Promise<Secret[] | null> {
|
||||||
return this._get(`/api/repos/${repoId}/secrets?page=${page}`) as Promise<Secret[] | null>;
|
return this._get(`/api/repos/${repoId}/secrets?page=${page}`) as Promise<Secret[] | null>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue