kubernetes
Release Notes Website
About Me
saschagrunert
mail@
.de
kubernetes
Governance
How to handle a project with
36.000 individual contributors
77.000 members on slack
1.200 companies involved
Special Interest Groups (SIGs)
API Machinery
Apps
Architecture
Auth
Autoscaling
CLI
Cloud Provider
Storage
Service Catalog
PM
Node
Network
Multicluster
Instrumentation
Docs
Contributor Experience
Cluster Lifecycle
Release
Scalability
Scheduling
Testing
UI
Usability
Windows
SIG Release
delivers a new kubernetes release every 3 month
around 40 people with different roles
Lead
Enhancements
CI Signal
Bug Triage
Docs
Release Notes
Communications
Patch Release Team
Branch Managers
Release Manager Associates
SIG Release Chairs
Associates
Release Notes Team
deliver the final kubernetes release notes
provide continuous release notes updates during the release cycle
works together with other SIGs to collect major features and known issues
The Website
relnotes.k8s.io
written in Angular 8
and TypeScript
backend-less hosted on netlify
continuously integrated via prow
unit-tested by Jest
end-to-end tested by cypress
Prow?
Kubernetes based CI/CD system
provides GitHub automation in the form of policy enforcement, chat-ops via /foo style commands, and automatic PR merging
Prow welcomes new contributors
automatic issue labeling
workflow management via approval and lgtm
automatic merge tide
Prow tests everything
Prow rebases before merging
From the Pull Request to the Website
Create a PR in kubernetes/kubernetes
Provide additional documentation if necessary
The final PR
Prow automatically labels and requests review
Graduate Volume Expansion to Beta + e2e tests #81467
Release Notes generation
go run cmd/release-notes/main.go
--output notes.json
--format json
--release-version 1.16.0
--start-rev v1.15.0
--end-rev v1.16.0
--github-token <my-secret-token>
the release notes tool is able to generate either Markdown or JSON
automatic generation for every patch release
{
"81467": {
"commit": "dccd28269a6596bf2f0041c14dd3233be8cfa571",
"text": "Move CSI volume expansion to beta.",
"markdown": "Move CSI volume expansion to beta. ([#81467](https://github.com/kubernetes/kubernetes/pull/81467), [@bertinatto](https://github.com/bertinatto))\n\n Courtesy of SIG Testing",
"documentation": [
{
"url": "https://github.com/kubernetes/enhancements/issues/556",
"type": "KEP"
}
],
"author": "bertinatto",
"author_url": "https://github.com/bertinatto",
"pr_url": "https://github.com/kubernetes/kubernetes/pull/81467",
"pr_number": 81467,
"areas": ["test"],
"kinds": ["feature"],
"sigs": ["testing"],
"feature": true,
"release_version": "1.16.0"
}
}
Consumable JSON
Release based static assets
over 3.600 commits per release
around 300 release notes per release
export const assets = [
'assets/release-notes-1.16.json',
'assets/release-notes-1.15.4.json',
'assets/release-notes-1.15.3.json',
'assets/release-notes-1.15.2.json',
'assets/release-notes-1.15.1.json',
'assets/release-notes-1.15.json',
];
src/environments/assets.ts
Mix between Redux and service based architecture
@Injectable({
providedIn: 'root',
})
export class NotesService {
constructor(private http: HttpClient, private logger: LoggerService) {}
/**
* Retrieve the notes
*
* @returns The NoteList as observable
*/
getNotes(): Observable<Note[]> {
this.logger.debug(`Gathering notes from ${assets.length} assets`);
const observables = [];
for (const asset of assets) {
observables.push(this.http.get(asset));
}
return forkJoin(observables).pipe(map(this.toNoteList));
}
/**
* Convert an array of any objects to a list of notes
*
* @returns The Note list
*/
toNoteList(jsonArray: any[]): Note[] {
const list = [];
for (let i = 0, len = jsonArray.length; i < len; i++) {
for (const value of Object.values(jsonArray[i])) {
list.push(value);
}
}
return list;
}
}
@Component({
selector: 'app-notes',
templateUrl: './notes.component.html',
providers: [],
styleUrls: ['./notes.component.scss'],
})
export class NotesComponent {
filter: Filter = new Filter();
allNotes: Note[] = [];
constructor(private store: Store<State>) {
this.store.dispatch(new GetNotes());
// ...
}
}
Notes retrieval and filtering via Redux
@Injectable()
export class NotesEffects {
@Effect()
getNotes$ = this.actions$.pipe(
ofType(ActionTypes.GetNotes),
exhaustMap(() =>
this.notesService.getNotes().pipe(
map((notes: Note[]) => {
this.logger.debug('[Notes Effects:GetNotes] SUCCESS');
return new GetNotesSuccess(notes);
}),
catchError(error => {
this.logger.debug(`[Notes Effects:GetNotes] FAILED: ${error}`);
return of(new Failed(error));
}),
),
),
);
}
Options abstraction
/**
* All available option types
*/
export enum OptionType {
areas = 'areas',
kinds = 'kinds',
releaseVersions = 'releaseVersions',
sigs = 'sigs',
documentation = 'documentation',
}
/**
* The option data type
*/
export type OptionData = Map<OptionType, OptionSet>;
/**
* The options for a single OptionType
*/
export type OptionSet = Set<string>;
/**
* A generic options abstraction
*/
export class Options {
/**
* The private data store
*/
private store: OptionData = new Map([
[OptionType.areas, new Set()],
[OptionType.kinds, new Set()],
[OptionType.releaseVersions, new Set()],
[OptionType.sigs, new Set()],
[OptionType.documentation, new Set()],
]);
/**
* Retrieve the data
*
* @returns OptionData The currently available data
*/
public get data(): OptionData {
return this.store;
}
/**
* Retrieve an single OptionSet for the provided optionType
*
* @param optionType The OptionType to be used
*
* @returns OptionSet The data
*/
public get(optionType: OptionType): OptionSet {
return this.data.get(optionType);
}
/**
* Append an input string array to the provided option type by discarding
* duplicate elements
*
* @param optionType The OptionType to be used
* @param input The array of documentation string to be added
*/
public add(optionType: OptionType, input: string[]) {
this.data.set(optionType, this.merge(this.data.get(optionType), input));
}
/**
* Merge a set and a string array into a new set
*
* @param input The base set
* @param arr The array to be appended
*
* @returns OptionSet the new Set of strings
*/
private merge(input: OptionSet, arr: string[]): OptionSet {
return new Set([...input, ...new Set(arr)]);
}
}
Filter abstraction
import { Params } from '@angular/router';
import { Options, OptionType, OptionSet } from './options.model';
/**
* A generic filter abstraction based on the Options type
*/
export class Filter extends Options {
/**
* The filter text
*/
public text = '';
/**
* Specifies of the filter should hide pre release versions
*/
public displayPreReleases = false;
/**
* The markdown key for the filter text
*/
public readonly markdownKey = 'markdown';
/**
* Helper method to test if the filter is empty
*
* @returns true is the filter is empty, false otherwise
*/
public isEmpty(): boolean {
if (this.text.length > 0) {
return false;
}
let empty = true;
this.data.forEach((value: OptionSet, key: string) => {
if (key !== OptionType.releaseVersions && value.size > 0) {
empty = false;
}
});
return empty;
}
/**
* Helper method to convert filter object into a URI-friendly object
*
* @returns Params the query parameter object
*/
public toURI(): Params {
const params = {};
// Set the markdown if needed
if (this.text.trim().length > 0) {
params[this.markdownKey] = this.text;
}
// Add the option data if needed
this.data.forEach((value: OptionSet, key: string) => {
if (value.size > 0) {
params[key] = [...value.values()];
}
});
return params;
}
/**
* Method to see if an option is empty
*
* @returns boolean true if empty, otherwise false
*/
public optionIsEmpty(optionType: OptionType): boolean {
if (super.get(optionType).size === 0) {
return true;
}
return false;
}
/**
* Method to add a value to an OptionType
*
* @param optionType The OptionType to be used
* @param value The value to be added
*/
public set(optionType: OptionType, value: string): void {
super.get(optionType).add(value);
}
/**
* Method to delete a value for an OptionType
*
* @param optionType The OptionType to be used
* @param value The value to be deleted
*/
public del(optionType: OptionType, value: string): void {
super.get(optionType).delete(value);
}
/**
* Method to check if the option type and value exists whtin the filter
*
* @param optionType The OptionType to be used
* @param value The value to be checked
*
* @returns boolean true if the filter contains the value, otherwise false
*/
public has(optionType: OptionType, value: string): boolean {
return super.get(optionType).has(value);
}
/**
* Method to check if the option type and value exists whtin the filter
*
* @param optionType The OptionType to be used
* @param values The values to be checked
*
* @returns boolean true if the filter contains any of the values, otherwise false
*/
public hasAny(optionType: OptionType, values: string[]): boolean {
if (values) {
for (const v of values) {
if (this.has(optionType, v)) {
return true;
}
}
}
return false;
}
/**
* Test wheter a release version string is a pre release
*
* @param version The release version string
*
* @returns boolean true if it's a pre-release, otherwise false
*/
public isPreRelease(version: string): boolean {
if (version) {
return version.includes('alpha') ||
version.includes('beta') ||
version.includes('rc');
}
return false;
}
}
Future Plans
full automation between drafting a release and the release notes website
features like RSS feeds, marking notes as read or a dedicated filter syntax
That’s it.
relnotes.k8s.io
github.com/kubernetes-sigs/release-notes
github.com/kubernetes/sig-release
#sig-release #release-notes on slack.k8s.io
The Kubernetes Release Notes Website
By Sascha Grunert
The Kubernetes Release Notes Website
A talk about the Kubernetes Release Notes tooling and website.
- 1,144