Europa docs: From local dev to CI environment doc page

The todoapp example contains a Netlify plan which uses the latest dagger
additions: do & Client API. We are thinking of merging the examples
repository into this one to make working with this easier. This is a
step in that direction.

We are not using the yarn package so that we can revert
https://github.com/dagger/dagger/pull/1673 without breaking this
implementation.

The GitHub Action is WIP, we will continue with that tomorrow:
https://github.com/dagger/dagger-for-github/issues/24

Signed-off-by: Gerhard Lazu <gerhard@lazu.co.uk>
This commit is contained in:
Gerhard Lazu 2022-03-07 15:42:15 +00:00
parent 2a6962ddc8
commit c3f21958d2
No known key found for this signature in database
GPG Key ID: A28DE70C9444D7A6
24 changed files with 11701 additions and 97 deletions

42
.github/workflows/todoapp.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: todoapp
on:
push:
branches:
- main
paths:
- '.github/workflows/todoapp.yml'
- 'pkg/universe.dagger.io/examples/todoapp/**'
env:
# This needs to be unique across all of Netlify
APP_NAME: todoapp-dagger-europa
NETLIFY_TEAM: blocklayer
# https://app.netlify.com/user/applications/personal
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
DAGGER_LOG_FORMAT: plain
jobs:
dagger:
name: "Deploy todoapp to Netlify"
runs-on: ubuntu-latest
steps:
- name: "Clone repository"
uses: actions/checkout@v2
with:
fetch-depth: 0
# TODO: maybe use Dagger action post 0.2.0-beta.1
- name: "Setup Go"
uses: actions/setup-go@v1
with:
go-version: 1.16
- name: "Install dev Dagger"
run: |
make dagger
- name: "Dagger"
run: |
cd pkg/universe.dagger.io/examples/todoapp
${{ github.workspace }}/cmd/dagger/dagger do deploy

View File

@ -5,6 +5,19 @@ displayed_sidebar: europa
# From local dev to CI environment
Dagger can be used with any CI environment (no migration required) and has two important advantages which make the overall experience less error-prone and more efficient:
1. Instead of YAML you write CUE: typed configuration with built-in formatting
2. Configuration is executed in buildkit, the execution engine at the heart of Docker
This makes any CI environment with Docker pre-installed work with Dagger out of the box.
We started with [CI environments that you told us you are using](https://github.com/dagger/dagger/discussions/1677).
We will configure a production deployment for the same application that we covered in the previous page.
:::note
If you cannot find your CI environment below, [let us know via this GitHub discussion](https://github.com/dagger/dagger/discussions/1677).
:::
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
<Tabs defaultValue="github-actions"
@ -13,23 +26,70 @@ values={[
{label: 'GitHub Actions', value: 'github-actions'},
{label: 'CircleCI', value: 'circleci'},
{label: 'GitLab', value: 'gitlab'},
{label: 'Jenkins', value: 'jenkins'},
{label: 'Tekton', value: 'tekton'},
]}>
<TabItem value="github-actions">
Since Dagger early access required a GitHub account, GitHub Actions seems like a reasonable starting point.
`.github/workflows/todoapp.yml`
```yaml
name: todoapp
push:
# Trigger this workflow only on commits pushed to the main branch
branches:
- main
# Dagger plan gets configured via client environment variables
env:
# This needs to be unique across all of netlify.app
APP_NAME: todoapp-dagger-europa
NETLIFY_TEAM: dagger
# https://app.netlify.com/user/applications/personal
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
DAGGER_LOG_FORMAT: plain
jobs:
dagger:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Deploy to Netlify
# https://github.com/dagger/dagger-for-github
uses: dagger/dagger-for-github@v0.2
with:
workdir: pkg/universe.dagger.io/examples/todoapp
plan: .
do: deploy
```
</TabItem>
<TabItem value="circleci">
If you would like us to document this CI environment next, mention it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
If you would like us to document CircleCI next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
</TabItem>
<TabItem value="gitlab">
If you would like us to document this CI environment next, mention it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
If you would like us to document GitLab next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
</TabItem>
<TabItem value="jenkins">
If you would like us to document Jenkins next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
</TabItem>
<TabItem value="tekton">
If you would like us to document Tekton next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
</TabItem>

View File

@ -0,0 +1 @@
build

View File

@ -0,0 +1,3 @@
# Todo APP
[Dagger documentation website](https://docs.dagger.io/)

View File

@ -1,28 +0,0 @@
// Deployment plan for Dagger's example todoapp
package todoapp
import (
"dagger.io/dagger"
"universe.dagger.io/git"
"universe.dagger.io/yarn"
)
dagger.#Plan & {
// Build the app with yarn
actions: build: yarn.#Build
// Wire up source code to build
{
input: directories: source: _
actions: build: source: input.directories.source.contents
} | {
actions: {
pull: git.#Pull & {
remote: "https://github.com/mdn/todo-react"
ref: "master"
}
build: source: pull.output
}
}
}

View File

@ -1,40 +0,0 @@
// Local dev environment for todoapp
package todoapp
import (
"universe.dagger.io/docker"
"universe.dagger.io/nginx"
)
// Expose todoapp web port
proxy: web: _
actions: {
// Reference app build inherited from base config
build: _
_app: build.output
container: {
// Build a container image serving the app with nginx
build: docker.#Build & {
steps: [
nginx.#Build & {
flavor: "alpine"
},
docker.#Copy & {
contents: _app
dest: "/usr/share/nginx/html"
},
]
}
// Run the app in an ephemeral container
run: docker.#Run & {
image: build.output
ports: web: {
frontend: proxy.web.endpoint
backend: address: "localhost:5000"
}
}
}
}

View File

@ -0,0 +1,94 @@
package netlify
import (
"dagger.io/dagger"
"universe.dagger.io/alpine"
"universe.dagger.io/bash"
"universe.dagger.io/docker"
"universe.dagger.io/netlify"
)
dagger.#Plan & {
client: {
filesystem: {
".": read: {
contents: dagger.#FS
exclude: [
"README.md",
"build",
"netlify.cue",
"node_modules",
]
}
build: write: contents: actions.build.contents.output
}
env: {
APP_NAME: string
NETLIFY_TEAM: string
NETLIFY_TOKEN: dagger.#Secret
}
}
actions: {
deps: docker.#Build & {
steps: [
alpine.#Build & {
packages: {
bash: {}
yarn: {}
git: {}
}
},
docker.#Copy & {
contents: client.filesystem.".".read.contents
dest: "/src"
},
// bash.#Run is a superset of docker.#Run
// install yarn dependencies
bash.#Run & {
workdir: "/src"
mounts: "/cache/yarn": dagger.#Mount & {
dest: "/cache/yarn"
type: "cache"
contents: dagger.#CacheDir & {
id: "todoapp-yarn-cache"
}
}
script: contents: #"""
yarn config set cache-folder /cache/yarn
yarn install
"""#
},
]
}
test: bash.#Run & {
input: deps.output
workdir: "/src"
script: contents: #"""
yarn run test
"""#
}
build: {
run: bash.#Run & {
input: test.output
workdir: "/src"
script: contents: #"""
yarn run build
"""#
}
contents: dagger.#Subdir & {
input: run.output.rootfs
path: "/src/build"
}
}
deploy: netlify.#Deploy & {
contents: build.contents.output
site: client.env.APP_NAME
token: client.env.NETLIFY_TOKEN
team: client.env.NETLIFY_TEAM
}
}
}

View File

@ -0,0 +1,38 @@
{
"name": "moz-todo-react",
"version": "0.1.0",
"private": true,
"homepage": "./",
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"gh-pages": "^3.2.3",
"nanoid": "^3.1.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --watchAll=false --passWithNoTests",
"gh-pages": "gh-pages -d build -u 'github-actions-bot <support+actions@github.com>'",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>My Todo app</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,120 @@
import React, { useState, useRef, useEffect } from "react";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";
import { nanoid } from "nanoid";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const FILTER_MAP = {
All: () => true,
Active: task => !task.completed,
Completed: task => task.completed
};
const FILTER_NAMES = Object.keys(FILTER_MAP);
function App(props) {
const [tasks, setTasks] = useState(props.tasks);
const [filter, setFilter] = useState('All');
function toggleTaskCompleted(id) {
const updatedTasks = tasks.map(task => {
// if this task has the same ID as the edited task
if (id === task.id) {
// use object spread to make a new obkect
// whose `completed` prop has been inverted
return {...task, completed: !task.completed}
}
return task;
});
setTasks(updatedTasks);
}
function deleteTask(id) {
const remainingTasks = tasks.filter(task => id !== task.id);
setTasks(remainingTasks);
}
function editTask(id, newName) {
const editedTaskList = tasks.map(task => {
// if this task has the same ID as the edited task
if (id === task.id) {
//
return {...task, name: newName}
}
return task;
});
setTasks(editedTaskList);
}
const taskList = tasks
.filter(FILTER_MAP[filter])
.map(task => (
<Todo
id={task.id}
name={task.name}
completed={task.completed}
key={task.id}
toggleTaskCompleted={toggleTaskCompleted}
deleteTask={deleteTask}
editTask={editTask}
/>
));
const filterList = FILTER_NAMES.map(name => (
<FilterButton
key={name}
name={name}
isPressed={name === filter}
setFilter={setFilter}
/>
));
function addTask(name) {
const newTask = { id: "todo-" + nanoid(), name: name, completed: false };
setTasks([...tasks, newTask]);
}
const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task';
const headingText = `${taskList.length} ${tasksNoun} remaining`;
const listHeadingRef = useRef(null);
const prevTaskLength = usePrevious(tasks.length);
useEffect(() => {
if (tasks.length - prevTaskLength === -1) {
listHeadingRef.current.focus();
}
}, [tasks.length, prevTaskLength]);
return (
<div className="todoapp stack-large">
<Form addTask={addTask} />
<div className="filters btn-group stack-exception">
{filterList}
</div>
<h2 id="list-heading" tabIndex="-1" ref={listHeadingRef}>
{headingText}
</h2>
<ul
className="todo-list stack-large stack-exception"
aria-labelledby="list-heading"
>
{taskList}
</ul>
</div>
);
}
export default App;

View File

@ -0,0 +1,18 @@
import React from "react";
function FilterButton(props) {
return (
<button
type="button"
className="btn toggle-btn"
aria-pressed={props.isPressed}
onClick={() => props.setFilter(props.name)}
>
<span className="visually-hidden">Show </span>
<span>{props.name}</span>
<span className="visually-hidden"> tasks</span>
</button>
);
}
export default FilterButton;

View File

@ -0,0 +1,45 @@
import React, { useState } from "react";
function Form(props) {
const [name, setName] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (!name.trim()) {
return;
}
props.addTask(name);
setName("");
}
function handleChange(e) {
setName(e.target.value);
}
return (
<form onSubmit={handleSubmit}>
<h2 className="label-wrapper">
<label htmlFor="new-todo-input" className="label__lg">
What needs to be done?
</label>
</h2>
<input
type="text"
id="new-todo-input"
className="input input__lg"
name="text"
autoComplete="off"
value={name}
onChange={handleChange}
/>
<button type="submit" className="btn btn__primary btn__lg">
Add
</button>
</form>
);
}
export default Form;

View File

@ -0,0 +1,113 @@
import React, { useEffect, useRef, useState } from "react";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
export default function Todo(props) {
const [isEditing, setEditing] = useState(false);
const [newName, setNewName] = useState('');
const editFieldRef = useRef(null);
const editButtonRef = useRef(null);
const wasEditing = usePrevious(isEditing);
function handleChange(e) {
setNewName(e.target.value);
}
function handleSubmit(e) {
e.preventDefault();
if (!newName.trim()) {
return;
}
props.editTask(props.id, newName);
setNewName("");
setEditing(false);
}
const editingTemplate = (
<form className="stack-small" onSubmit={handleSubmit}>
<div className="form-group">
<label className="todo-label" htmlFor={props.id}>
New name for {props.name}
</label>
<input
id={props.id}
className="todo-text"
type="text"
value={newName || props.name}
onChange={handleChange}
ref={editFieldRef}
/>
</div>
<div className="btn-group">
<button
type="button"
className="btn todo-cancel"
onClick={() => setEditing(false)}
>
Cancel
<span className="visually-hidden">renaming {props.name}</span>
</button>
<button type="submit" className="btn btn__primary todo-edit">
Save
<span className="visually-hidden">new name for {props.name}</span>
</button>
</div>
</form>
);
const viewTemplate = (
<div className="stack-small">
<div className="c-cb">
<input
id={props.id}
type="checkbox"
defaultChecked={props.completed}
onChange={() => props.toggleTaskCompleted(props.id)}
/>
<label className="todo-label" htmlFor={props.id}>
{props.name}
</label>
</div>
<div className="btn-group">
<button
type="button"
className="btn"
onClick={() => setEditing(true)}
ref={editButtonRef}
>
Edit <span className="visually-hidden">{props.name}</span>
</button>
<button
type="button"
className="btn btn__danger"
onClick={() => props.deleteTask(props.id)}
>
Delete <span className="visually-hidden">{props.name}</span>
</button>
</div>
</div>
);
useEffect(() => {
if (!wasEditing && isEditing) {
editFieldRef.current.focus();
}
if (wasEditing && !isEditing) {
editButtonRef.current.focus();
}
}, [wasEditing, isEditing]);
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
}

View File

@ -0,0 +1,293 @@
/* RESETS */
*,
*::before,
*::after {
box-sizing: border-box;
}
*:focus {
outline: 3px dashed #228bec;
outline-offset: 0;
}
html {
font: 62.5% / 1.15 sans-serif;
}
h1,
h2 {
margin-bottom: 0;
}
ul {
list-style: none;
padding: 0;
}
button {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
color: inherit;
font: inherit;
line-height: normal;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
}
button::-moz-focus-inner {
border: 0;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
input[type="text"] {
border-radius: 0;
}
body {
width: 100%;
max-width: 68rem;
margin: 0 auto;
font: 1.6rem/1.25 Arial, sans-serif;
background-color: #f5f5f5;
color: #4d4d4d;
}
@media screen and (min-width: 620px) {
body {
font-size: 1.9rem;
line-height: 1.31579;
}
}
/*END RESETS*/
/* GLOBAL STYLES */
.form-group > input[type="text"] {
display: inline-block;
margin-top: 0.4rem;
}
.btn {
padding: 0.8rem 1rem 0.7rem;
border: 0.2rem solid #4d4d4d;
cursor: pointer;
text-transform: capitalize;
}
.btn.toggle-btn {
border-width: 1px;
border-color: #d3d3d3;
}
.btn.toggle-btn[aria-pressed="true"] {
text-decoration: underline;
border-color: #4d4d4d;
}
.btn__danger {
color: #fff;
background-color: #ca3c3c;
border-color: #bd2130;
}
.btn__filter {
border-color: lightgrey;
}
.btn__primary {
color: #fff;
background-color: #000;
}
.btn-group {
display: flex;
justify-content: space-between;
}
.btn-group > * {
flex: 1 1 49%;
}
.btn-group > * + * {
margin-left: 0.8rem;
}
.label-wrapper {
margin: 0;
flex: 0 0 100%;
text-align: center;
}
.visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
[class*="stack"] > * {
margin-top: 0;
margin-bottom: 0;
}
.stack-small > * + * {
margin-top: 1.25rem;
}
.stack-large > * + * {
margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
.stack-small > * + * {
margin-top: 1.4rem;
}
.stack-large > * + * {
margin-top: 2.8rem;
}
}
.stack-exception {
margin-top: 1.2rem;
}
/* END GLOBAL STYLES */
.todoapp {
background: #fff;
margin: 2rem 0 4rem 0;
padding: 1rem;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
@media screen and (min-width: 550px) {
.todoapp {
padding: 4rem;
}
}
.todoapp > * {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
}
.todoapp > form {
max-width: 100%;
}
.todoapp > h1 {
display: block;
max-width: 100%;
text-align: center;
margin: 0;
margin-bottom: 1rem;
}
.label__lg {
line-height: 1.01567;
font-weight: 300;
padding: 0.8rem;
margin-bottom: 1rem;
text-align: center;
}
.input__lg {
padding: 2rem;
border: 2px solid #000;
}
.input__lg:focus {
border-color: #4d4d4d;
box-shadow: inset 0 0 0 2px;
}
[class*="__lg"] {
display: inline-block;
width: 100%;
font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
[class*="__lg"] {
font-size: 2.4rem;
}
}
.filters {
width: 100%;
margin: unset auto;
}
/* Todo item styles */
.todo {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.todo > * {
flex: 0 0 100%;
}
.todo-text {
width: 100%;
min-height: 4.4rem;
padding: 0.4rem 0.8rem;
border: 2px solid #565656;
}
.todo-text:focus {
box-shadow: inset 0 0 0 2px;
}
/* CHECKBOX STYLES */
.c-cb {
box-sizing: border-box;
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
font-weight: 400;
font-size: 1.6rem;
line-height: 1.25;
display: block;
position: relative;
min-height: 44px;
padding-left: 40px;
clear: left;
}
.c-cb > label::before,
.c-cb > input[type="checkbox"] {
box-sizing: border-box;
top: -2px;
left: -2px;
width: 44px;
height: 44px;
}
.c-cb > input[type="checkbox"] {
-webkit-font-smoothing: antialiased;
cursor: pointer;
position: absolute;
z-index: 1;
margin: 0;
opacity: 0;
}
.c-cb > label {
font-size: inherit;
font-family: inherit;
line-height: inherit;
display: inline-block;
margin-bottom: 0;
padding: 8px 15px 5px;
cursor: pointer;
touch-action: manipulation;
}
.c-cb > label::before {
content: "";
position: absolute;
border: 2px solid currentColor;
background: transparent;
}
.c-cb > input[type="checkbox"]:focus + label::before {
border-width: 4px;
outline: 3px dashed #228bec;
}
.c-cb > label::after {
box-sizing: content-box;
content: "";
position: absolute;
top: 11px;
left: 9px;
width: 18px;
height: 7px;
transform: rotate(-45deg);
border: solid;
border-width: 0 0 5px 5px;
border-top-color: transparent;
opacity: 0;
background: transparent;
}
.c-cb > input[type="checkbox"]:checked + label::after {
opacity: 1;
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
const DATA = [
{ id: "todo-0", name: "Eat", completed: true },
{ id: "todo-1", name: "Sleep", completed: false },
{ id: "todo-2", name: "Repeat", completed: false }
];
ReactDOM.render(
<React.StrictMode>
<App tasks={DATA} />
</React.StrictMode>,
document.getElementById('root')
);

View File

@ -1,25 +0,0 @@
// Deploy to Netlify
package todoapp
import (
"universe.dagger.io/netlify"
)
// Netlify API token
input: secrets: netlify: _
// Must be a valid branch/PR name
environment: string
actions: {
// Yarn build inherited from base config
build: _
deploy: netlify.#Deploy & {
contents: build.output
token: input.secrets.netlify.contents
site: *"acme-inc-\(environment)" | string
team: *"acme-inc" | string
}
}

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,9 @@ if [ -z "$site_id" ] ; then
if [ -z "$site_id" ]; then
echo "create site failed"
exit 1
else
echo "clean create site API response..."
rm -f body
fi
fi

View File

@ -67,7 +67,7 @@ module.exports = {
{
type: "link",
label: "🆕 Dagger Europa 🆕",
href: "/1201/ci-environment",
href: "/1200/local-dev",
},
],
europa: [