mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Implement actions (#21937)
Close #13539. Co-authored by: @lunny @appleboy @fuxiaohei and others. Related projects: - https://gitea.com/gitea/actions-proto-def - https://gitea.com/gitea/actions-proto-go - https://gitea.com/gitea/act - https://gitea.com/gitea/act_runner ### Summary The target of this PR is to bring a basic implementation of "Actions", an internal CI/CD system of Gitea. That means even though it has been merged, the state of the feature is **EXPERIMENTAL**, and please note that: - It is disabled by default; - It shouldn't be used in a production environment currently; - It shouldn't be used in a public Gitea instance currently; - Breaking changes may be made before it's stable. **Please comment on #13539 if you have any different product design ideas**, all decisions reached there will be adopted here. But in this PR, we don't talk about **naming, feature-creep or alternatives**. ### ⚠️ Breaking `gitea-actions` will become a reserved user name. If a user with the name already exists in the database, it is recommended to rename it. ### Some important reviews - What is `DEFAULT_ACTIONS_URL` in `app.ini` for? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954 - Why the api for runners is not under the normal `/api/v1` prefix? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592 - Why DBFS? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178 - Why ignore events triggered by `gitea-actions` bot? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103 - Why there's no permission control for actions? - https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868 ### What it looks like <details> #### Manage runners <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png"> #### List runs <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png"> #### View logs <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png"> </details> ### How to try it <details> #### 1. Start Gitea Clone this branch and [install from source](https://docs.gitea.io/en-us/install-from-source). Add additional configurations in `app.ini` to enable Actions: ```ini [actions] ENABLED = true ``` Start it. If all is well, you'll see the management page of runners: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png"> #### 2. Start runner Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow the [README](https://gitea.com/gitea/act_runner/src/branch/main/README.md) to start it. If all is well, you'll see a new runner has been added: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png"> #### 3. Enable actions for a repo Create a new repo or open an existing one, check the `Actions` checkbox in settings and submit. <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png"> <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png"> If all is well, you'll see a new tab "Actions": <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png"> #### 4. Upload workflow files Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can follow the [quickstart](https://docs.github.com/en/actions/quickstart) of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions in most cases, you can use the same demo: ```yaml name: GitHub Actions Demo run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 on: [push] jobs: Explore-GitHub-Actions: runs-on: ubuntu-latest steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - name: Check out repository code uses: actions/checkout@v3 - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner." - name: List files in the repository run: | ls ${{ github.workspace }} - run: echo "🍏 This job's status is ${{ job.status }}." ``` If all is well, you'll see a new run in `Actions` tab: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png"> #### 5. Check the logs of jobs Click a run and you'll see the logs: <img width="1792" alt="image" src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png"> #### 6. Go on You can try more examples in [the documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) of GitHub Actions, then you might find a lot of bugs. Come on, PRs are welcome. </details> See also: [Feature Preview: Gitea Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/) --------- Co-authored-by: a1012112796 <1012112796@qq.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-authored-by: John Olheiser <john.olheiser@gmail.com>
This commit is contained in:
		
							
								
								
									
										398
									
								
								web_src/js/components/RepoActionView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								web_src/js/components/RepoActionView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,398 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="action-view-container">
 | 
			
		||||
    <div class="action-view-header">
 | 
			
		||||
      <div class="action-info-summary">
 | 
			
		||||
        {{ run.title }}
 | 
			
		||||
        <button class="run_cancel" @click="cancelRun()" v-if="run.canCancel">
 | 
			
		||||
          <i class="stop circle outline icon"/>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="action-view-body">
 | 
			
		||||
      <div class="action-view-left">
 | 
			
		||||
        <div class="job-group-section">
 | 
			
		||||
          <div class="job-brief-list">
 | 
			
		||||
            <a class="job-brief-item" v-for="(job, index) in run.jobs" :key="job.id" :href="run.htmlurl+'/jobs/'+index">
 | 
			
		||||
              <SvgIcon name="octicon-check-circle-fill" class="green" v-if="job.status === 'success'"/>
 | 
			
		||||
              <SvgIcon name="octicon-skip" class="ui text grey" v-else-if="job.status === 'skipped'"/>
 | 
			
		||||
              <SvgIcon name="octicon-clock" class="ui text yellow" v-else-if="job.status === 'waiting'"/>
 | 
			
		||||
              <SvgIcon name="octicon-blocked" class="ui text yellow" v-else-if="job.status === 'blocked'"/>
 | 
			
		||||
              <SvgIcon name="octicon-meter" class="ui text yellow" class-name="job-status-rotate" v-else-if="job.status === 'running'"/>
 | 
			
		||||
              <SvgIcon name="octicon-x-circle-fill" class="red" v-else/>
 | 
			
		||||
              {{ job.name }}
 | 
			
		||||
              <button class="job-brief-rerun" @click="rerunJob(index)" v-if="job.canRerun">
 | 
			
		||||
                <SvgIcon name="octicon-sync" class="ui text black"/>
 | 
			
		||||
              </button>
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="action-view-right">
 | 
			
		||||
        <div class="job-info-header">
 | 
			
		||||
          <div class="job-info-header-title">
 | 
			
		||||
            {{ currentJob.title }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="job-info-header-detail">
 | 
			
		||||
            {{ currentJob.detail }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="job-step-container">
 | 
			
		||||
          <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i">
 | 
			
		||||
            <div class="job-step-summary" @click.stop="toggleStepLogs(i)">
 | 
			
		||||
              <SvgIcon name="octicon-chevron-down" class="mr-3" v-show="currentJobStepsStates[i].expanded"/>
 | 
			
		||||
              <SvgIcon name="octicon-chevron-right" class="mr-3" v-show="!currentJobStepsStates[i].expanded"/>
 | 
			
		||||
 | 
			
		||||
              <SvgIcon name="octicon-check-circle-fill" class="green mr-3" v-if="jobStep.status === 'success'"/>
 | 
			
		||||
              <SvgIcon name="octicon-skip" class="ui text grey mr-3" v-else-if="jobStep.status === 'skipped'"/>
 | 
			
		||||
              <SvgIcon name="octicon-clock" class="ui text yellow mr-3" v-else-if="jobStep.status === 'waiting'"/>
 | 
			
		||||
              <SvgIcon name="octicon-blocked" class="ui text yellow mr-3" v-else-if="jobStep.status === 'blocked'"/>
 | 
			
		||||
              <SvgIcon name="octicon-meter" class="ui text yellow mr-3" class-name="job-status-rotate" v-else-if="jobStep.status === 'running'"/>
 | 
			
		||||
              <SvgIcon name="octicon-x-circle-fill" class="red mr-3 " v-else/>
 | 
			
		||||
 | 
			
		||||
              <span class="step-summary-msg">{{ jobStep.summary }}</span>
 | 
			
		||||
              <span class="step-summary-dur">{{ jobStep.duration }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM -->
 | 
			
		||||
            <div class="job-step-logs" ref="logs" v-show="currentJobStepsStates[i].expanded"/>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {SvgIcon} from '../svg.js';
 | 
			
		||||
import {createApp} from 'vue';
 | 
			
		||||
import AnsiToHTML from 'ansi-to-html';
 | 
			
		||||
 | 
			
		||||
const {csrfToken} = window.config;
 | 
			
		||||
 | 
			
		||||
const sfc = {
 | 
			
		||||
  name: 'RepoActionView',
 | 
			
		||||
  components: {
 | 
			
		||||
    SvgIcon,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    runIndex: String,
 | 
			
		||||
    jobIndex: String,
 | 
			
		||||
    actionsURL: String,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      ansiToHTML: new AnsiToHTML({escapeXML: true}),
 | 
			
		||||
 | 
			
		||||
      // internal state
 | 
			
		||||
      loading: false,
 | 
			
		||||
      intervalID: null,
 | 
			
		||||
      currentJobStepsStates: [],
 | 
			
		||||
 | 
			
		||||
      // provided by backend
 | 
			
		||||
      run: {
 | 
			
		||||
        htmlurl: '',
 | 
			
		||||
        title: '',
 | 
			
		||||
        canCancel: false,
 | 
			
		||||
        done: false,
 | 
			
		||||
        jobs: [
 | 
			
		||||
          // {
 | 
			
		||||
          //   id: 0,
 | 
			
		||||
          //   name: '',
 | 
			
		||||
          //   status: '',
 | 
			
		||||
          //   canRerun: false,
 | 
			
		||||
          // },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      currentJob: {
 | 
			
		||||
        title: '',
 | 
			
		||||
        detail: '',
 | 
			
		||||
        steps: [
 | 
			
		||||
          // {
 | 
			
		||||
          //   summary: '',
 | 
			
		||||
          //   duration: '',
 | 
			
		||||
          //   status: '',
 | 
			
		||||
          // }
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  mounted() {
 | 
			
		||||
    // load job data and then auto-reload periodically
 | 
			
		||||
    this.loadJob();
 | 
			
		||||
    this.intervalID = setInterval(this.loadJob, 1000);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
    // get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
 | 
			
		||||
    getLogsContainer(idx) {
 | 
			
		||||
      const el = this.$refs.logs[idx];
 | 
			
		||||
      return el._stepLogsActiveContainer ?? el;
 | 
			
		||||
    },
 | 
			
		||||
    // begin a log group
 | 
			
		||||
    beginLogGroup(idx) {
 | 
			
		||||
      const el = this.$refs.logs[idx];
 | 
			
		||||
 | 
			
		||||
      const elJobLogGroup = document.createElement('div');
 | 
			
		||||
      elJobLogGroup.classList.add('job-log-group');
 | 
			
		||||
 | 
			
		||||
      const elJobLogGroupSummary = document.createElement('div');
 | 
			
		||||
      elJobLogGroupSummary.classList.add('job-log-group-summary');
 | 
			
		||||
 | 
			
		||||
      const elJobLogList = document.createElement('div');
 | 
			
		||||
      elJobLogList.classList.add('job-log-list');
 | 
			
		||||
 | 
			
		||||
      elJobLogGroup.appendChild(elJobLogGroupSummary);
 | 
			
		||||
      elJobLogGroup.appendChild(elJobLogList);
 | 
			
		||||
      el._stepLogsActiveContainer = elJobLogList;
 | 
			
		||||
    },
 | 
			
		||||
    // end a log group
 | 
			
		||||
    endLogGroup(idx) {
 | 
			
		||||
      const el = this.$refs.logs[idx];
 | 
			
		||||
      el._stepLogsActiveContainer = null;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // show/hide the step logs for a step
 | 
			
		||||
    toggleStepLogs(idx) {
 | 
			
		||||
      this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded;
 | 
			
		||||
      if (this.currentJobStepsStates[idx].expanded) {
 | 
			
		||||
        this.loadJob(); // try to load the data immediately instead of waiting for next timer interval
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    // rerun a job
 | 
			
		||||
    rerunJob(idx) {
 | 
			
		||||
      this.fetch(`${this.run.htmlurl}/jobs/${idx}/rerun`);
 | 
			
		||||
    },
 | 
			
		||||
    // cancel a run
 | 
			
		||||
    cancelRun() {
 | 
			
		||||
      this.fetch(`${this.run.htmlurl}/cancel`);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    createLogLine(line) {
 | 
			
		||||
      const div = document.createElement('div');
 | 
			
		||||
      div.classList.add('job-log-line');
 | 
			
		||||
      div._jobLogTime = line.timestamp;
 | 
			
		||||
 | 
			
		||||
      const lineNumber = document.createElement('div');
 | 
			
		||||
      lineNumber.className = 'line-num';
 | 
			
		||||
      lineNumber.innerText = line.index;
 | 
			
		||||
      div.appendChild(lineNumber);
 | 
			
		||||
 | 
			
		||||
      // TODO: Support displaying time optionally
 | 
			
		||||
 | 
			
		||||
      const logMessage = document.createElement('div');
 | 
			
		||||
      logMessage.className = 'log-msg';
 | 
			
		||||
      logMessage.innerHTML = this.ansiToHTML.toHtml(line.message);
 | 
			
		||||
      div.appendChild(logMessage);
 | 
			
		||||
 | 
			
		||||
      return div;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    appendLogs(stepIndex, logLines) {
 | 
			
		||||
      for (const line of logLines) {
 | 
			
		||||
        // TODO: group support: ##[group]GroupTitle , ##[endgroup]
 | 
			
		||||
        const el = this.getLogsContainer(stepIndex);
 | 
			
		||||
        el.append(this.createLogLine(line));
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async fetchJob() {
 | 
			
		||||
      const logCursors = this.currentJobStepsStates.map((it, idx) => {
 | 
			
		||||
        // cursor is used to indicate the last position of the logs
 | 
			
		||||
        // it's only used by backend, frontend just reads it and passes it back, it and can be any type.
 | 
			
		||||
        // for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
 | 
			
		||||
        return {step: idx, cursor: it.cursor, expanded: it.expanded};
 | 
			
		||||
      });
 | 
			
		||||
      const resp = await this.fetch(
 | 
			
		||||
        `${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`,
 | 
			
		||||
        JSON.stringify({logCursors}),
 | 
			
		||||
      );
 | 
			
		||||
      return await resp.json();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async loadJob() {
 | 
			
		||||
      if (this.loading) return;
 | 
			
		||||
      try {
 | 
			
		||||
        this.loading = true;
 | 
			
		||||
 | 
			
		||||
        const response = await this.fetchJob();
 | 
			
		||||
 | 
			
		||||
        // save the state to Vue data, then the UI will be updated
 | 
			
		||||
        this.run = response.state.run;
 | 
			
		||||
        this.currentJob = response.state.currentJob;
 | 
			
		||||
 | 
			
		||||
        // sync the currentJobStepsStates to store the job step states
 | 
			
		||||
        for (let i = 0; i < this.currentJob.steps.length; i++) {
 | 
			
		||||
          if (!this.currentJobStepsStates[i]) {
 | 
			
		||||
            this.currentJobStepsStates[i] = {cursor: null, expanded: false};
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        // append logs to the UI
 | 
			
		||||
        for (const logs of response.logs.stepsLog) {
 | 
			
		||||
          // save the cursor, it will be passed to backend next time
 | 
			
		||||
          this.currentJobStepsStates[logs.step].cursor = logs.cursor;
 | 
			
		||||
          this.appendLogs(logs.step, logs.lines);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.run.done && this.intervalID) {
 | 
			
		||||
          clearInterval(this.intervalID);
 | 
			
		||||
          this.intervalID = null;
 | 
			
		||||
        }
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.loading = false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    fetch(url, body) {
 | 
			
		||||
      return fetch(url, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json',
 | 
			
		||||
          'X-Csrf-Token': csrfToken,
 | 
			
		||||
        },
 | 
			
		||||
        body,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default sfc;
 | 
			
		||||
 | 
			
		||||
export function initRepositoryActionView() {
 | 
			
		||||
  const el = document.getElementById('repo-action-view');
 | 
			
		||||
  if (!el) return;
 | 
			
		||||
 | 
			
		||||
  const view = createApp(sfc, {
 | 
			
		||||
    runIndex: el.getAttribute('data-run-index'),
 | 
			
		||||
    jobIndex: el.getAttribute('data-job-index'),
 | 
			
		||||
    actionsURL: el.getAttribute('data-actions-url'),
 | 
			
		||||
  });
 | 
			
		||||
  view.mount(el);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
 | 
			
		||||
// some elements are not managed by vue, so we need to use _actions.less in addition.
 | 
			
		||||
 | 
			
		||||
.action-view-body {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  height: calc(100vh - 266px); // fine tune this value to make the main view has full height
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ================
 | 
			
		||||
// action view header
 | 
			
		||||
 | 
			
		||||
.action-view-header {
 | 
			
		||||
  margin: 0 20px 20px 20px;
 | 
			
		||||
  button.run_cancel {
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: var(--color-red);
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition:transform 0.2s;
 | 
			
		||||
  };
 | 
			
		||||
  button.run_cancel:hover{
 | 
			
		||||
    transform:scale(130%);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.action-info-summary {
 | 
			
		||||
  font-size: 150%;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  padding: 0 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ================
 | 
			
		||||
// action view left
 | 
			
		||||
 | 
			
		||||
.action-view-left {
 | 
			
		||||
  width: 30%;
 | 
			
		||||
  max-width: 400px;
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.job-group-section {
 | 
			
		||||
  .job-group-summary {
 | 
			
		||||
    margin: 5px 0;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .job-brief-list {
 | 
			
		||||
    a.job-brief-item {
 | 
			
		||||
      display: block;
 | 
			
		||||
      margin: 5px 0;
 | 
			
		||||
      padding: 10px;
 | 
			
		||||
      background: var(--color-info-bg);
 | 
			
		||||
      border-radius: 5px;
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
      button.job-brief-rerun {
 | 
			
		||||
        float: right;
 | 
			
		||||
        border: none;
 | 
			
		||||
        background-color: transparent;
 | 
			
		||||
        outline: none;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        transition:transform 0.2s;
 | 
			
		||||
      };
 | 
			
		||||
      button.job-brief-rerun:hover{
 | 
			
		||||
        transform:scale(130%);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    a.job-brief-item:hover {
 | 
			
		||||
      background-color: var(--color-secondary);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ================
 | 
			
		||||
// action view right
 | 
			
		||||
 | 
			
		||||
.action-view-right {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  background-color: var(--color-console-bg);
 | 
			
		||||
  color: var(--color-console-fg);
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.job-info-header {
 | 
			
		||||
  .job-info-header-title {
 | 
			
		||||
    font-size: 150%;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
  }
 | 
			
		||||
  .job-info-header-detail {
 | 
			
		||||
    padding: 0 10px 10px;
 | 
			
		||||
    border-bottom: 1px solid var(--color-grey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.job-step-container {
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
 | 
			
		||||
  .job-step-summary {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    padding: 5px 10px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
 | 
			
		||||
    .step-summary-msg {
 | 
			
		||||
      flex: 1;
 | 
			
		||||
    }
 | 
			
		||||
    .step-summary-dur {
 | 
			
		||||
      margin-left: 16px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .job-step-summary:hover {
 | 
			
		||||
    background-color: var(--color-black-light);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +90,7 @@ import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
 | 
			
		||||
import {initFormattingReplacements} from './features/formatting.js';
 | 
			
		||||
import {initMcaptcha} from './features/mcaptcha.js';
 | 
			
		||||
import {initCopyContent} from './features/copycontent.js';
 | 
			
		||||
import {initRepositoryActionView} from './components/RepoActionView.vue';
 | 
			
		||||
 | 
			
		||||
// Run time-critical code as soon as possible. This is safe to do because this
 | 
			
		||||
// script appears at the end of <body> and rendered HTML is accessible at that point.
 | 
			
		||||
@@ -187,6 +188,7 @@ $(document).ready(() => {
 | 
			
		||||
  initRepoTopicBar();
 | 
			
		||||
  initRepoWikiForm();
 | 
			
		||||
  initRepository();
 | 
			
		||||
  initRepositoryActionView();
 | 
			
		||||
 | 
			
		||||
  initCommitStatuses();
 | 
			
		||||
  initMcaptcha();
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,16 @@ import octiconSidebarCollapse from '../../public/img/svg/octicon-sidebar-collaps
 | 
			
		||||
import octiconSidebarExpand from '../../public/img/svg/octicon-sidebar-expand.svg';
 | 
			
		||||
import octiconTriangleDown from '../../public/img/svg/octicon-triangle-down.svg';
 | 
			
		||||
import octiconX from '../../public/img/svg/octicon-x.svg';
 | 
			
		||||
import octiconCheckCircleFill from '../../public/img/svg/octicon-check-circle-fill.svg';
 | 
			
		||||
import octiconXCircleFill from '../../public/img/svg/octicon-x-circle-fill.svg';
 | 
			
		||||
import octiconSkip from '../../public/img/svg/octicon-skip.svg';
 | 
			
		||||
import octiconMeter from '../../public/img/svg/octicon-meter.svg';
 | 
			
		||||
import octiconBlocked from '../../public/img/svg/octicon-blocked.svg';
 | 
			
		||||
import octiconSync from '../../public/img/svg/octicon-sync.svg';
 | 
			
		||||
 | 
			
		||||
export const svgs = {
 | 
			
		||||
  'octicon-blocked': octiconBlocked,
 | 
			
		||||
  'octicon-check-circle-fill': octiconCheckCircleFill,
 | 
			
		||||
  'octicon-chevron-down': octiconChevronDown,
 | 
			
		||||
  'octicon-chevron-right': octiconChevronRight,
 | 
			
		||||
  'octicon-clock': octiconClock,
 | 
			
		||||
@@ -44,6 +52,7 @@ export const svgs = {
 | 
			
		||||
  'octicon-kebab-horizontal': octiconKebabHorizontal,
 | 
			
		||||
  'octicon-link': octiconLink,
 | 
			
		||||
  'octicon-lock': octiconLock,
 | 
			
		||||
  'octicon-meter': octiconMeter,
 | 
			
		||||
  'octicon-milestone': octiconMilestone,
 | 
			
		||||
  'octicon-mirror': octiconMirror,
 | 
			
		||||
  'octicon-project': octiconProject,
 | 
			
		||||
@@ -52,8 +61,11 @@ export const svgs = {
 | 
			
		||||
  'octicon-repo-template': octiconRepoTemplate,
 | 
			
		||||
  'octicon-sidebar-collapse': octiconSidebarCollapse,
 | 
			
		||||
  'octicon-sidebar-expand': octiconSidebarExpand,
 | 
			
		||||
  'octicon-skip': octiconSkip,
 | 
			
		||||
  'octicon-sync': octiconSync,
 | 
			
		||||
  'octicon-triangle-down': octiconTriangleDown,
 | 
			
		||||
  'octicon-x': octiconX,
 | 
			
		||||
  'octicon-x-circle-fill': octiconXCircleFill,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const parser = new DOMParser();
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user