Ü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.
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)
| Adresse | Route (name) |
Props bzw. weitere Angaben |
|---|---|---|
/gesuche/3 |
||
/haltepunkte/1/gesuche |
||
/gesuche/3/bearbeiten |
||
/haltepunkte/eins/gesuche |
||
/gesuche?haltepunkt=1 |
| 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/2zeigt den Treffpunkt „Hauptbahnhof, ZOB Steig 3, Gütersloh“, die Richtung „zur Schule“ und das Zeitfenster „2026-10-21, 06:30–07:00 Uhr“./gesuche/42zeigt „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 buildSie 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-only1 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 daysJe 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.jsonsteht die Abhängigkeitpinia, in der committetenpackage-lock.jsonfehlt sie, etwa weilpackage.jsonvon Hand geändert oder die Lock-Datei nicht committet wurde.npm cibricht dann schon imbefore_scriptvonlintab;testundbuildwürden genauso scheitern und werden deshalb gar nicht erst gestartet. Lokal warpiniaschon installiert bzw. wurde mitnpm installinstalliert, das die Lock-Datei stillschweigend anpasst.B2: Die neue Komponente
Hilfe.vueverletzt die ESLint-Regelvue/multi-word-component-names(einwortige Namen könnten mit HTML-Elementen zusammenfallen). Das Paar hat vor dem Push nurnpm testausgeführt, nichtnpm 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
releasehat die Regel$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/. Das Tag0.2.0beginnt nicht mitv, der Job wird nicht in die Pipeline aufgenommen. Lokal lässt sich das nicht bemerken, weil Tags undruleserst in GitLab ausgewertet werden;npm version minorhätte das Tagv0.2.0gesetzt.
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 installlokal ausführen, damitpackage-lock.jsondie neue Abhängigkeit enthält, und beide Dateien gemeinsam committen; neue Pakete immer mitnpm install <paket>statt von Hand eintragen.B2 (1 Punkt): Komponente umbenennen, z. B.
HilfeSeite.vue(auch Import und Route anpassen); vor jedem Pushnpm run lintausführen, etwa als gemeinsames Skriptnpm 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 (VariableTZ) 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.0auf denselben Commit setzen und pushen (git tag v0.2.0 0.2.0bzw. 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 mitnpm versionsetzen, das Präfixvist 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).
Links und weitere Informationen
zum Nachlesen: Vue Router und Continuous Integration: Detailseite, Pipeline und das erste Small Release der Vue-App
zum Nachlesen: API-Client aus OpenAPI mit dem erzeugten Client und Mock Service Worker
zum Szenario: Szenario Mitfahrlabor mit Personas und Regeln
als Nachschlagewerk: Vue Router: Dokumentation und GitLab: CI/CD YAML syntax reference
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]
