Übungsaufgaben zu Vue Router und Continuous Integration: Routen lesen, Gesuch-Detailseite, Pipeline aufbauen und Fehler finden

https://bildung.social/@oerinformatik/

https://oer-informatik.de/vue-router-continuous-integration-uebungsaufgaben

Vue Router, Routenparameter, Single-Page-Application, Continuous Integration, GitLab CI/CD, Pipeline, ESLint, Small Releases, Fehlersuche, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (ca. 50 min Lese- und Bearbeitungszeit): Die Web-App des Mitfahrlabors bekommt Seiten für die Mitfahrgesuche und eine Pipeline. In diesen Übungen bestimmst Du zu Adressen Route, Parameter und Props, baust test-first eine Detailseite für ein Gesuch mit eigener Route, teilst eine Pipeline in Stages mit Cache, Testbericht, Artefakten und Release-Job auf und findest anhand von Job-Protokollen heraus, warum eine Pipeline rot ist [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über die Web-App mit Vue 3 und TypeScript: Projektübersicht / XP-Projektstart / TypeScript für Java-Entwickler*innen / Komponenten testgetrieben / API-Client aus OpenAPI / Router und Continuous Integration / Übungsaufgaben zu Router und CI / Formulare und Validierung / Refactoring mit Pinia / Anmeldung mit Keycloak / Fahrer-Sicht und Coding Standards / Build und Auslieferung / End-to-End-Tests mit Playwright / Change Request Live-Aktualisierung

Diese Übungsaufgaben gehören zum Artikel Vue Router und Continuous Integration: Detailseite, Pipeline und das erste Small Release der Vue-App. Alle Aufgaben spielen im Projektteam der Mitfahr-App Mitfahrlabor. Ausgangspunkt für Aufgabe 2.1 ist das Projekt mitfahrlabor-web im Ordner src/05-vue-router-continuous-integration des Repositorys dieser Reihe; alles, was für die Gesuche noch fehlt, steht vollständig in der Aufgabe.

Überblick: Ein git push oder ein Tag startet die Pipeline mit den Stages pruefen, testen, bauen und veroeffentlichen; Ergebnisse sind der Testbericht, die gebaute App und bei Versions-Tags ein Release (mit KI erstellt)

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Notiere Deine Antworten zuerst vollständig, bevor Du die Beispiellösung aufklappst. Für Aufgabe 2.1 brauchst Du Node.js ab Version 24; führe nach jedem Schritt npm test und am Ende npm run lint und npm run type-check aus. Ein laufendes Backend brauchst Du nicht, die Tests arbeiten mit Mock Service Worker. Aufgabe 2.2 lässt sich ohne GitLab bearbeiten; wer ein GitLab-Projekt hat, kann die Lösung im Pipeline-Editor prüfen. Andere fachlich richtige Lösungen, etwa mit anderen Namen für Tests, Funktionen, Jobs oder CSS-Klassen, sind ebenso richtig.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „Vue Router und Continuous
Integration: Routen lesen, Gesuch-Detailseite, Pipeline aufbauen und
Fehler finden“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/vue-router-continuous-integration
- https://oer-informatik.de/szenario-mitfahrlabor
- https://oer-informatik.de/vue-openapi-client
- Stilbeispiel: https://oer-informatik.de/vue-router-continuous-integration-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Vue Router
- Routenparameter
- Single-Page-Application
- Continuous Integration
- GitLab CI/CD
- Pipeline
- ESLint
- Small Releases
- Fehlersuche

Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 1.1: Routen lesen (Basiskompetenz)

Für die Mitfahrgesuche hat das Team die Routentabelle der Web-App erweitert. Die Seiten GesucheSeite und GesuchSeite sollen im nächsten Schritt entstehen:

export const routes: RouteRecordRaw[] = [
  { path: '/', name: 'suche', component: SucheSeite },
  { path: '/gesuche', name: 'gesuche', component: () => import('@/views/GesucheSeite.vue') },
  {
    path: '/gesuche/:gesuchId(\\d+)',
    name: 'gesuch',
    component: () => import('@/views/GesuchSeite.vue'),
    props: (route) => ({ gesuchId: Number(route.params.gesuchId) }),
  },
  {
    path: '/haltepunkte/:haltepunktId(\\d+)/gesuche',
    name: 'gesuche-am-haltepunkt',
    component: () => import('@/views/GesucheSeite.vue'),
    props: true,
  },
  { path: '/:pfad(.*)*', name: 'nicht-gefunden', component: NichtGefundenSeite },
]

a) Bestimme für jede Adresse den Namen der Route, die Vue Router auswählt, und die Props, die die Seite bekommt (mit Datentyp). Steht in der Adresse ein Teil hinter ?, gib auch an, wo die Seite ihn findet. (5 Punkte)

Adressen und Routen
Adresse Route (name) Props bzw. weitere Angaben
/gesuche/3
/haltepunkte/1/gesuche
/gesuche/3/bearbeiten
/haltepunkte/eins/gesuche
/gesuche?haltepunkt=1
Adressen und Routen (Lösung)
Adresse Route (name) Props bzw. weitere Angaben
/gesuche/3 gesuch gesuchId = 3 als Zahl (über die props-Funktion)
/haltepunkte/1/gesuche gesuche-am-haltepunkt haltepunktId = '1' als Text (props: true reicht den Parameter unverändert durch)
/gesuche/3/bearbeiten nicht-gefunden keine; pfad ist ['gesuche', '3', 'bearbeiten']
/haltepunkte/eins/gesuche nicht-gefunden keine; eins passt nicht auf \d+
/gesuche?haltepunkt=1 gesuche keine Props; der Teil hinter ? ist kein Pfad, sondern steht in route.query.haltepunkt ('1')

Je Zeile 1 Punkt; für die erste und zweite Zeile nur mit richtigem Datentyp.

b) Beim Ausprobieren stellt das Team fest, dass GesucheSeite je nach Adresse eine Haltepunkt-Nummer als Text statt als Zahl oder gar nicht bekommt. Erkläre, welche Änderung an der Route gesuche-am-haltepunkt dafür sorgt, dass haltepunktId immer als Zahl ankommt, und warum die Seite dann ohne Router testbar bleibt. (2 Punkte)

Statt props: true eine Funktion wie bei gesuch: props: (route) => ({ haltepunktId: Number(route.params.haltepunktId) }) (1 Punkt). Die Seite liest dann nicht selbst useRoute(), sondern bekommt eine gewöhnliche Prop; im Test übergibt man sie mit mount(GesucheSeite, { props: { haltepunktId: 1 } }) ohne Router (1 Punkt).

c) Auf der Seite /gesuche/2 gibt es einen Link „nächstes Gesuch“ zu /gesuche/3. In einer ersten Fassung lädt GesuchSeite das Gesuch in onMounted. Sage voraus, was nach einem Klick auf den Link in der Adresszeile und auf der Seite steht, und begründe Deine Vorhersage. (3 Punkte)

In der Adresszeile steht /gesuche/3 (1 Punkt), die Seite zeigt aber weiter Gesuch 2 (1 Punkt). Beide Adressen passen auf dieselbe Route gesuch; Vue Router verwendet die vorhandene Komponente weiter und ändert nur die Prop gesuchId. onMounted läuft nicht erneut. Abhilfe: mit watch(() => props.gesuchId, …, { immediate: true }) laden (1 Punkt).

Aufgabe 2.1: Eine Detailseite für Mitfahrgesuche (Kernkompetenz)

Mitfahrende sollen ein Gesuch über eine eigene Adresse aufrufen können, etwa aus einer Nachricht an Fahrer*innen heraus. Thomas Becker formuliert dafür zwei Kundentests:

  • /gesuche/2 zeigt den Treffpunkt „Hauptbahnhof, ZOB Steig 3, Gütersloh“, die Richtung „zur Schule“ und das Zeitfenster „2026-10-21, 06:30–07:00 Uhr“.

  • /gesuche/42 zeigt „Gesuch nicht gefunden“.

Ein zurückgezogenes Gesuch soll zusätzlich den Hinweis „Dieses Gesuch ist nicht mehr offen.“ zeigen.

Startcode: Ergänze in api/openapi.yaml unter paths den neuen Pfad (vor components:) und unter components/schemas die beiden Schemas (falls Du sie aus den Übungen zum API-Client schon hast, nur den Pfad):

  /gesuche/{gesuchId}:
    get:
      tags: [gesuche]
      operationId: holeGesuch
      summary: ein Mitfahrgesuch abrufen
      parameters:
        - name: gesuchId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: das Gesuch
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Gesuch'
        '404':
          $ref: '#/components/responses/NichtGefunden'
    GesuchStatus:
      type: string
      enum: [offen, vermittelt, zurueckgezogen, abgelaufen]
    Gesuch:
      type: object
      required: [id, mitfahrerId, haltepunktId, richtung, datum, zeitVon, zeitBis, status]
      properties:
        id:
          type: integer
          format: int64
        mitfahrerId:
          type: integer
          format: int64
        haltepunktId:
          type: integer
          format: int64
        richtung:
          $ref: '#/components/schemas/Richtung'
        datum:
          type: string
          format: date
        zeitVon:
          type: string
          pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
        zeitBis:
          type: string
          pattern: '^([01][0-9]|2[0-3]):[0-5][0-9]$'
        status:
          $ref: '#/components/schemas/GesuchStatus'

Die Testdaten kommen nach src/mocks/gesuche.ts:

import type { Gesuch } from '@/api'

// Testdaten: Gesuche von Oksana Melnyk (3), Lina Hartwig (8), Lukas Hofmann (4) und Aylin Kaya (2)
export const testGesuche: Gesuch[] = [
  { id: 1, mitfahrerId: 3, haltepunktId: 1, richtung: 'zur-schule', datum: '2026-10-26', zeitVon: '06:45', zeitBis: '07:15', status: 'offen' },
  { id: 2, mitfahrerId: 8, haltepunktId: 1, richtung: 'zur-schule', datum: '2026-10-21', zeitVon: '06:30', zeitBis: '07:00', status: 'offen' },
  { id: 3, mitfahrerId: 4, haltepunktId: 1, richtung: 'nach-hause', datum: '2026-10-19', zeitVon: '14:00', zeitBis: '15:00', status: 'offen' },
  { id: 4, mitfahrerId: 2, haltepunktId: 3, richtung: 'zur-schule', datum: '2026-10-22', zeitVon: '06:40', zeitBis: '07:10', status: 'offen' },
  { id: 5, mitfahrerId: 3, haltepunktId: 1, richtung: 'nach-hause', datum: '2026-10-21', zeitVon: '14:15', zeitBis: '15:00', status: 'zurueckgezogen' },
]

a) Erzeuge den Client neu und ergänze in src/mocks/handlers.ts einen typisierten Handler für GET /v1/gesuche/:gesuchId, der das Gesuch aus testGesuche liefert oder mit Status 404 antwortet. (2 Punkte)

npm run api:erzeugen erzeugt holeGesuch() und die Typen Gesuch, GesuchStatus, HoleGesuchResponse (1 Punkt). Handler (1 Punkt):

import type { HoleAngebotResponse, HoleGesuchResponse, Problem, SucheAngeboteResponse } from '@/api'
import { testGesuche } from './gesuche'

// im Array handlers ergänzen:
http.get<{ gesuchId: string }, never, HoleGesuchResponse | Problem>(`${API}/gesuche/:gesuchId`, ({ params }) => {
  const gesuch = testGesuche.find((g) => g.id === Number(params.gesuchId))
  if (!gesuch) {
    return HttpResponse.json({ title: 'Not Found', status: 404 }, { status: 404 })
  }
  return HttpResponse.json(gesuch)
}),

b) Ergänze die Routentabelle in src/router/index.ts um eine benannte Route für /gesuche/:gesuchId, die nur Ziffern akzeptiert, die Seite GesuchSeite erst bei Bedarf lädt und gesuchId als Zahl übergibt. Schreibe dazu einen Test, der zeigt, dass /gesuche/4 auf diese Route führt und /gesuche/vier nicht. (3 Punkte)

Route vor der Catch-all-Route (2 Punkte: Pfad mit (\\d+), Lazy Loading, props-Funktion mit Number):

{
  path: '/gesuche/:gesuchId(\\d+)',
  name: 'gesuch',
  component: () => import('@/views/GesuchSeite.vue'),
  props: (route) => ({ gesuchId: Number(route.params.gesuchId) }),
},

Test (1 Punkt):

it('ist über /gesuche/4 erreichbar', () => {
  const router = erzeugeRouter(createMemoryHistory())
  expect(router.resolve('/gesuche/4').name).toBe('gesuch')
  expect(router.resolve('/gesuche/vier').name).toBe('nicht-gefunden')
})

c) Schreibe zuerst die Tests für GesuchSeite in src/views/__tests__/GesuchSeite.api.spec.ts: die beiden Kundentests, den Hinweis für das zurückgezogene Gesuch 5 und einen Test, der die App mit echtem Router unter /gesuche/4 startet. Begründe, warum Du RouterLinkStub brauchst, obwohl die Seite selbst keinen Router benötigt. (4 Punkte)

import { describe, it, expect, vi } from 'vitest'
import { mount, RouterLinkStub } from '@vue/test-utils'
import { createMemoryHistory } from 'vue-router'
import GesuchSeite from '@/views/GesuchSeite.vue'
import App from '@/App.vue'
import { erzeugeRouter } from '@/router'

const global = { stubs: { RouterLink: RouterLinkStub } }

describe('GesuchSeite', () => {
  it('zeigt Treffpunkt, Richtung und Zeitfenster von Gesuch 2', async () => {
    const wrapper = mount(GesuchSeite, { props: { gesuchId: 2 }, global })
    expect(wrapper.text()).toContain('Gesuch wird geladen')
    await vi.waitFor(() => expect(wrapper.find('dl').exists()).toBe(true))
    expect(wrapper.get('.treffpunkt').text()).toBe('Hauptbahnhof, ZOB Steig 3, Gütersloh')
    expect(wrapper.get('.richtung').text()).toBe('zur Schule')
    expect(wrapper.get('.zeitfenster').text()).toBe('2026-10-21, 06:30–07:00 Uhr')
    expect(wrapper.find('[role=status]').exists()).toBe(false)
  })

  it('weist auf ein zurückgezogenes Gesuch hin', async () => {
    const wrapper = mount(GesuchSeite, { props: { gesuchId: 5 }, global })
    await vi.waitFor(() => expect(wrapper.find('[role=status]').exists()).toBe(true))
    expect(wrapper.get('[role=status]').text()).toBe('Dieses Gesuch ist nicht mehr offen.')
  })

  it('meldet „Gesuch nicht gefunden“ für Gesuch 42', async () => {
    const wrapper = mount(GesuchSeite, { props: { gesuchId: 42 }, global })
    await vi.waitFor(() => expect(wrapper.find('h2').exists()).toBe(true))
    expect(wrapper.get('h2').text()).toBe('Gesuch nicht gefunden')
  })

  it('ist über /gesuche/4 erreichbar', async () => {
    const router = erzeugeRouter(createMemoryHistory())
    await router.push('/gesuche/4')
    const wrapper = mount(App, { global: { plugins: [router] } })
    await vi.waitFor(() => expect(wrapper.find('.treffpunkt').exists()).toBe(true))
    expect(wrapper.get('.treffpunkt').text()).toBe('Marktplatz, Halle (Westf.)')
  })
})

Je Test 0,5 Punkte für den Ablauf (Laden abwarten mit vi.waitFor), 1 Punkt für die richtigen Erwartungen insgesamt. Begründung (1 Punkt): Die Seite enthält den Link „Zurück zur Fahrtensuche“ als RouterLink; ohne Router kann Vue ihn nicht auflösen. RouterLinkStub ersetzt ihn durch einen einfachen Link, sodass die Seite ohne Router montiert werden kann.

d) Schreibe die Ladefunktion ladeGesuch(gesuchId) in src/domain/gesuchdetail.ts und die Seite src/views/GesuchSeite.vue, bis alle Tests grün sind. Ein 404 soll kein Fehler, sondern ein eigenes Ergebnis sein; ist das Backend nicht erreichbar, zeigt die Seite „Das Gesuch kann gerade nicht geladen werden.“ (5 Punkte)

src/domain/gesuchdetail.ts (2 Punkte):

import { holeGesuch, type Gesuch } from '@/api'

export type GesuchErgebnis =
  | { art: 'gefunden'; gesuch: Gesuch }
  | { art: 'nicht-gefunden'; meldung: string }
  | { art: 'fehler'; meldung: string }

const FEHLER: GesuchErgebnis = { art: 'fehler', meldung: 'Das Gesuch kann gerade nicht geladen werden.' }

export async function ladeGesuch(gesuchId: number, wartezeitMs = 5000): Promise<GesuchErgebnis> {
  try {
    const { data, response } = await holeGesuch({ path: { gesuchId }, signal: AbortSignal.timeout(wartezeitMs) })
    if (response?.status === 404) return { art: 'nicht-gefunden', meldung: 'Gesuch nicht gefunden' }
    if (!response?.ok || data === undefined) return FEHLER
    return { art: 'gefunden', gesuch: data }
  } catch {
    return FEHLER
  }
}

src/views/GesuchSeite.vue (3 Punkte: Laden per watch mit immediate, Anzeige je Ergebnis, Hinweis bei Status ungleich offen):

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { RouterLink } from 'vue-router'
import { ladeGesuch, type GesuchErgebnis } from '@/domain/gesuchdetail'
import { richtungText } from '@/domain/anzeige'
import { haltepunkte } from '@/domain/beispieldaten'

const props = defineProps<{ gesuchId: number }>()
const ergebnis = ref<GesuchErgebnis>()

watch(
  () => props.gesuchId,
  async (id) => {
    ergebnis.value = undefined
    ergebnis.value = await ladeGesuch(id)
  },
  { immediate: true },
)

const haltepunkt = computed(() => {
  const e = ergebnis.value
  return e?.art === 'gefunden' ? haltepunkte.find((h) => h.id === e.gesuch.haltepunktId) : undefined
})
</script>

<template>
  <section>
    <p v-if="!ergebnis" class="text-secondary">Gesuch wird geladen …</p>
    <template v-else-if="ergebnis.art === 'gefunden'">
      <h2>Gesuch {{ ergebnis.gesuch.id }}</h2>
      <dl class="row">
        <dt class="col-sm-3">Treffpunkt</dt>
        <dd class="col-sm-9 treffpunkt">{{ haltepunkt?.name }}, {{ haltepunkt?.ort }}</dd>
        <dt class="col-sm-3">Richtung</dt>
        <dd class="col-sm-9 richtung">{{ richtungText(ergebnis.gesuch.richtung) }}</dd>
        <dt class="col-sm-3">Zeitfenster</dt>
        <dd class="col-sm-9 zeitfenster">{{ ergebnis.gesuch.datum }}, {{ ergebnis.gesuch.zeitVon }}–{{ ergebnis.gesuch.zeitBis }} Uhr</dd>
      </dl>
      <p v-if="ergebnis.gesuch.status !== 'offen'" class="alert alert-secondary" role="status">Dieses Gesuch ist nicht mehr offen.</p>
    </template>
    <h2 v-else-if="ergebnis.art === 'nicht-gefunden'">{{ ergebnis.meldung }}</h2>
    <p v-else class="alert alert-danger" role="alert">{{ ergebnis.meldung }}</p>
    <RouterLink :to="{ name: 'suche' }">Zurück zur Fahrtensuche</RouterLink>
  </section>
</template>

Die Hilfsvariable e im computed ist nötig: Innerhalb der Pfeilfunktion von find merkt sich TypeScript die Prüfung ergebnis.value?.art === 'gefunden' nicht, ergebnis.value.gesuch wäre ein Typfehler (vue-tsc meldet TS2339). Mit allen Teilaufgaben laufen 49 Tests grün; npm run lint und npm run type-check melden nichts.

Aufgabe 2.2: Eine Pipeline in Stages aufteilen (Kernkompetenz)

Das Team hat für die ersten Tage eine Pipeline aus einem einzigen Job angelegt:

alles:
  image: node:24-alpine
  script:
    - npm install
    - npm test
    - npm run build

Sie funktioniert, aber jede Pipeline lädt alle Pakete neu herunter, ein Linter fehlt, die Testergebnisse erscheinen nicht im Merge Request, und die gebaute App ist nach dem Job verloren.

a) Erkläre, warum in einer Pipeline npm ci statt npm install stehen sollte. (2 Punkte)

npm ci installiert genau die Versionen aus package-lock.json in ein leeres node_modules (1 Punkt) und bricht ab, wenn package.json und package-lock.json nicht zusammenpassen; npm install würde die Lock-Datei stillschweigend anpassen, sodass die Pipeline etwas anderes baut als das, was im Repository steht (1 Punkt).

b) Teile die Pipeline in die Stages pruefen (Lint und Typprüfung), testen und bauen auf. Gemeinsame Einstellungen (Image, Installation, Cache des npm-Zwischenspeichers, der sich nur bei geänderter package-lock.json erneuert) sollen nur einmal in der Datei stehen. (4 Punkte)

stages:
  - pruefen
  - testen
  - bauen

default:
  image: node:24-alpine
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
  before_script:
    - npm ci --cache .npm --prefer-offline

lint:
  stage: pruefen
  script:
    - npm run lint
    - npm run type-check

test:
  stage: testen
  script:
    - npm run test:ci

build:
  stage: bauen
  script:
    - npm run build-only

1 Punkt für stages und die Zuordnung der Jobs, 1 Punkt für default mit Image und before_script, 1 Punkt für den Cache mit key:files, 1 Punkt für die Aufteilung von Lint, Typprüfung, Test und Build.

c) Ergänze, dass GitLab die Testergebnisse im Merge Request anzeigt, auch wenn Tests scheitern, und dass die gebaute App 30 Tage zum Herunterladen bereitsteht. Gib auch das nötige Skript test:ci für package.json an. (3 Punkte)

"test:ci": "vitest run --reporter=default --reporter=junit --outputFile.junit=reports/junit.xml"
test:
  stage: testen
  script:
    - npm run test:ci
  artifacts:
    when: always
    reports:
      junit: reports/junit.xml

build:
  stage: bauen
  script:
    - npm run build-only
  artifacts:
    paths:
      - dist/
    expire_in: 30 days

Je 1 Punkt für das Skript, den JUnit-Bericht mit when: always und die Artefakte von build.

d) Ergänze einen Job in einer Stage veroeffentlichen, der nur bei Tags der Form v1.2.3 ein Release in GitLab anlegt. (2 Punkte)

veroeffentlichen in stages ergänzen und:

release:
  stage: veroeffentlichen
  image: registry.gitlab.com/gitlab-org/cli:latest
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  cache: []
  before_script: []
  script:
    - echo "Release $CI_COMMIT_TAG wird angelegt"
  release:
    tag_name: $CI_COMMIT_TAG
    name: "Mitfahrlabor Web $CI_COMMIT_TAG"
    description: "Änderungen siehe CHANGELOG.md"

1 Punkt für rules mit dem Muster, 1 Punkt für release mit passendem Image und abgeschalteten Vorgaben (before_script: [], sonst scheitert npm ci im Container ohne Node.js).

e) Nach Iteration 3 soll Version 0.2.0 an Thomas gehen. Gib die Befehle an, mit denen Du die Version setzt und das Release auslöst, und begründe die Versionsnummer. (2 Punkte)

npm version minor
git push --follow-tags

npm version minor setzt 0.1.0 → 0.2.0 in package.json und package-lock.json, committet und setzt das Tag v0.2.0; git push --follow-tags überträgt Commit und Tag und startet die Pipeline mit dem Release-Job (1 Punkt). Iteration 3 bringt neue Funktionen (Platz anfragen, Fahrt anbieten) ohne inkompatible Änderung, nach Semantic Versioning steigt deshalb die zweite Stelle (1 Punkt). Vorher CHANGELOG.md ergänzen.

Aufgabe 3.1: Rote Pipelines analysieren (Profilkompetenz)

In einer Woche sind vier Pipelines des Mitfahrlabors nicht wie erwartet gelaufen. Die Pipeline ist die aus dem Artikel (Jobs lint, test, build, release). Die Paare berichten jeweils: „Lokal lief alles.“

B1: lint ist nach wenigen Sekunden rot, test und build wurden übersprungen. Im Protokoll von lint steht:

$ npm ci --cache .npm --prefer-offline
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Missing: pinia@4.1.0 from lock file
...
ERROR: Job failed: exit code 1

B2: lint ist rot, test und build wurden übersprungen:

$ npm run lint
> mitfahrlabor-web@0.1.0 lint
> eslint .

/builds/mitfahrlabor/mitfahrlabor-web/src/views/Hilfe.vue
  1:1  error  Component name "Hilfe" should always be multi-word  vue/multi-word-component-names

ERROR: Job failed: exit code 1

B3: test ist rot. Ein Paar hatte für die Vorbelegung des Suchformulars eine Funktion mit Test geschrieben:

/** Datum des Rechners als JJJJ-MM-TT (Ortszeit), Vorbelegung für das Suchformular */
export function heuteIso(jetzt: Date = new Date()): string {
  const monat = String(jetzt.getMonth() + 1).padStart(2, '0')
  const tag = String(jetzt.getDate()).padStart(2, '0')
  return `${jetzt.getFullYear()}-${monat}-${tag}`
}
it('liefert kurz nach Mitternacht schon den neuen Tag', () => {
  expect(heuteIso(new Date('2026-10-19T23:30:00Z'))).toBe('2026-10-20')
})
 FAIL  src/__tests__/datum.spec.ts > heuteIso > liefert kurz nach Mitternacht schon den neuen Tag
AssertionError: expected '2026-10-19' to be '2026-10-20' // Object.is equality
Expected: "2026-10-20"
Received: "2026-10-19"

B4: Für das Release nach Iteration 3 hat ein Paar das Tag 0.2.0 gesetzt und gepusht. Die Pipeline des Tags zeigt lint, test und build grün, ein Release gibt es in GitLab aber nicht.

a) Bestimme für B1 bis B4 jeweils die Ursache und erkläre, warum das Problem lokal nicht aufgefallen ist. (8 Punkte)

Je Fall 1 Punkt für die Ursache, 1 Punkt für die Erklärung:

  • B1: In package.json steht die Abhängigkeit pinia, in der committeten package-lock.json fehlt sie, etwa weil package.json von Hand geändert oder die Lock-Datei nicht committet wurde. npm ci bricht dann schon im before_script von lint ab; test und build würden genauso scheitern und werden deshalb gar nicht erst gestartet. Lokal war pinia schon installiert bzw. wurde mit npm install installiert, das die Lock-Datei stillschweigend anpasst.

  • B2: Die neue Komponente Hilfe.vue verletzt die ESLint-Regel vue/multi-word-component-names (einwortige Namen könnten mit HTML-Elementen zusammenfallen). Das Paar hat vor dem Push nur npm test ausgeführt, nicht npm run lint; Tests und Build bemerken die Regel nicht.

  • B3: Der Test erzeugt mit new Date('2026-10-19T23:30:00Z') einen Zeitpunkt in UTC. In Deutschland (Sommerzeit, UTC+2) ist das der 20.10. um 01:30 Uhr, der Test ist lokal grün. Der Container der Pipeline läuft in UTC; dort ist es der 19.10. um 23:30 Uhr. Die Funktion ist richtig, der Test hängt von der Zeitzone des Rechners ab.

  • B4: Der Job release hat die Regel $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/. Das Tag 0.2.0 beginnt nicht mit v, der Job wird nicht in die Pipeline aufgenommen. Lokal lässt sich das nicht bemerken, weil Tags und rules erst in GitLab ausgewertet werden; npm version minor hätte das Tag v0.2.0 gesetzt.

b) Gib für jeden Fall eine Korrektur an. Für B3 begründe, warum Du lieber den Test als die Pipeline änderst. (6 Punkte)

  • B1 (1 Punkt): npm install lokal ausführen, damit package-lock.json die neue Abhängigkeit enthält, und beide Dateien gemeinsam committen; neue Pakete immer mit npm install <paket> statt von Hand eintragen.

  • B2 (1 Punkt): Komponente umbenennen, z. B. HilfeSeite.vue (auch Import und Route anpassen); vor jedem Push npm run lint ausführen, etwa als gemeinsames Skript npm run lint && npm test.

  • B3 (2 Punkte): Im Test den Zeitpunkt in Ortszeit erzeugen, z. B. heuteIso(new Date(2026, 9, 20, 0, 30)), und '2026-10-20' erwarten; dieser Test ist in jeder Zeitzone grün. Begründung: Eine Zeitzone für die Pipeline (Variable TZ) würde den Fehler nur verdecken; der Test würde auf jedem Rechner in einer anderen Zeitzone wieder scheitern. Tests sollen unabhängig von Zeitzone, Sprache und Uhrzeit des Rechners sein.

  • B4 (2 Punkte): Das Tag v0.2.0 auf denselben Commit setzen und pushen (git tag v0.2.0 0.2.0 bzw. auf den Commit, git push origin v0.2.0), das falsche Tag löschen (git tag -d 0.2.0, git push origin :refs/tags/0.2.0). Künftig Versionen mit npm version setzen, das Präfix v ist dort Standard.

c) B1 hat den Hauptzweig an einem Vormittag blockiert, weil das Paar erst nach der Mittagspause reagiert hat. Beschreibe, wie das Team nach den Regeln von Continuous Integration hätte vorgehen sollen, und nenne eine Einstellung in GitLab, die verhindert, dass ein roter Stand überhaupt in den Hauptzweig gelangt. (4 Punkte)

Ein roter Build hat Vorrang vor neuer Arbeit (1 Punkt): Das Paar, das gepusht hat, repariert ihn sofort (1 Punkt); gelingt das nicht in etwa zehn Minuten, nimmt es die Änderung mit git revert zurück, damit alle anderen wieder auf einem grünen Stand arbeiten (1 Punkt). Vorbeugend: über kurze Zweige und Merge Requests arbeiten und in GitLab einstellen, dass ein Merge Request erst zusammengeführt werden kann, wenn seine Pipeline erfolgreich war („Pipelines must succeed“) (1 Punkt).


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu Vue Router und Continuous Integration: Routen lesen, Gesuch-Detailseite, Pipeline aufbauen und Fehler finden” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/vue-router-continuous-integration-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/programmierung/vue. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: