Full Code of whywaita/myshoes for AI

master d20faef23bff cached
93 files
350.2 KB
106.4k tokens
505 symbols
1 requests
Download .txt
Showing preview only (375K chars total). Download the full file or copy to clipboard to get everything.
Repository: whywaita/myshoes
Branch: master
Commit: d20faef23bff
Files: 93
Total size: 350.2 KB

Directory structure:
gitextract_98bm4mpl/

├── .github/
│   └── workflows/
│       ├── build-docker-sha.yaml
│       ├── release.yaml
│       └── test.yaml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api/
│   ├── myshoes/
│   │   ├── README.md
│   │   ├── client.go
│   │   ├── http.go
│   │   └── target.go
│   ├── proto/
│   │   └── myshoes.proto
│   └── proto.go/
│       ├── myshoes.pb.go
│       └── myshoes_grpc.pb.go
├── cmd/
│   ├── server/
│   │   └── cmd.go
│   └── shoes-tester/
│       └── main.go
├── docs/
│   ├── 01_01_for_admin_setup.md
│   ├── 01_02_for_admin_tips.md
│   ├── 02_01_for_user_setup.md
│   ├── 03_how-to-develop-shoes.md
│   └── assets/
│       └── myshoes.service
├── go.mod
├── go.sum
├── internal/
│   ├── testutils/
│   │   ├── mysql.go
│   │   ├── testutils.go
│   │   └── web.go
│   └── util/
│       └── util.go
└── pkg/
    ├── config/
    │   ├── config.go
    │   └── init.go
    ├── datastore/
    │   ├── github.go
    │   ├── interface.go
    │   ├── memory/
    │   │   └── memory.go
    │   ├── mysql/
    │   │   ├── job.go
    │   │   ├── job_test.go
    │   │   ├── lock.go
    │   │   ├── mysql.go
    │   │   ├── mysql_test.go
    │   │   ├── runner.go
    │   │   ├── runner_test.go
    │   │   ├── schema.sql
    │   │   ├── target.go
    │   │   └── target_test.go
    │   └── resource_type.go
    ├── docker/
    │   └── ratelimit.go
    ├── gh/
    │   ├── github.go
    │   ├── github_test.go
    │   ├── installation.go
    │   ├── jwt.go
    │   ├── jwt_test.go
    │   ├── label.go
    │   ├── metrics.go
    │   ├── metrics_test.go
    │   ├── ratelimit.go
    │   ├── runner.go
    │   ├── scope.go
    │   ├── token_registration.go
    │   ├── webhook.go
    │   ├── workflow_job.go
    │   └── workflow_run.go
    ├── logger/
    │   └── logger.go
    ├── metric/
    │   ├── collector.go
    │   ├── scrape_datastore.go
    │   ├── scrape_github.go
    │   ├── scrape_memory.go
    │   └── webhook.go
    ├── runner/
    │   ├── metrics.go
    │   ├── runner.go
    │   ├── runner_delete.go
    │   ├── runner_delete_ephemeral.go
    │   ├── runner_delete_once.go
    │   ├── token_update.go
    │   └── util.go
    ├── shoes/
    │   └── shoes.go
    ├── starter/
    │   ├── README.md
    │   ├── error.go
    │   ├── metric.go
    │   ├── metrics.go
    │   ├── safety/
    │   │   ├── README.md
    │   │   ├── safety.go
    │   │   └── unlimited/
    │   │       └── unlimited.go
    │   ├── scripts/
    │   │   └── RunnerService.js
    │   ├── scripts.go
    │   └── starter.go
    └── web/
        ├── config.go
        ├── http.go
        ├── http_test.go
        ├── metrics.go
        ├── target.go
        ├── target_create.go
        ├── target_test.go
        └── webhook.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/build-docker-sha.yaml
================================================
name: Build Docker image (sha)
on:
  push:
    branches:
      - "**"
  workflow_dispatch:

jobs:
  docker-build-sha:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
      - name: Cache Docker layers
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      - name: Login to GitHub Container Registry
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
        id: meta
        with:
          images: ghcr.io/${{ github.repository_owner }}/myshoes
          tags: |
            type=sha
      - name: Build container image
        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}


================================================
FILE: .github/workflows/release.yaml
================================================
name: release
on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0
      - name: Setup Go
        uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
        with:
          go-version-file: 'go.mod'
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
        with:
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
      - name: Login to GitHub Container Registry
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
        id: meta
        with:
          images: ghcr.io/whywaita/myshoes
          tags: |
            type=raw,value=latest
            type=semver,pattern={{raw}}
            type=sha
      - name: Build container image
        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}


================================================
FILE: .github/workflows/test.yaml
================================================
name: test
on:
  push:
    branches:
      - "**"
  pull_request:
  workflow_dispatch:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os:
          - ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 1
      - name: setup go
        uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
        with:
          go-version-file: 'go.mod'
      - name: lint
        run: |
          go install honnef.co/go/tools/cmd/staticcheck@latest
          staticcheck ./...
      - name: vet
        run: |
          go vet ./...
      - name: test
        run: |
          make test
  docker-build-test:
    runs-on: ubuntu-latest
    steps:
     - name: checkout
       uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
       with:
         fetch-depth: 1
     - name: Build container image
       uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
       with:
         push: false
         tags: ${{ steps.meta.outputs.tags }}


================================================
FILE: .gitignore
================================================

# Created by https://www.toptal.com/developers/gitignore/api/macos,intellij,go
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,intellij,go

### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

### Go Patch ###
/vendor/
/Godeps/

### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/

# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml

# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/

# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$

# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

# End of https://www.toptal.com/developers/gitignore/api/macos,intellij,go

/myshoes*

================================================
FILE: .goreleaser.yml
================================================
builds:
  - main: ./cmd/server/cmd.go
    goos:
      - linux
    goarch:
      - amd64
      - arm64

================================================
FILE: Dockerfile
================================================
FROM golang:1.25 AS builder

WORKDIR /go/src/github.com/whywaita/myshoes

RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
RUN apt-get update -y \
    && apt-get install -y protobuf-compiler

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

COPY . .
RUN make build-linux

FROM alpine

RUN apk update \
  && apk update
RUN apk add --no-cache ca-certificates \
  && update-ca-certificates 2>/dev/null || true

COPY --from=builder /go/src/github.com/whywaita/myshoes/myshoes-linux-amd64 /app

CMD ["/app"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 whywaita

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: Makefile
================================================
.PHONY: help
.DEFAULT_GOAL := help

CURRENT_REVISION = $(shell git rev-parse --short HEAD)
BUILD_LDFLAGS = "-X main.revision=$(CURRENT_REVISION)"

help:
	@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

build: ## Build All
	go generate ./...
	make build-proto
	go build -o myshoes -ldflags $(BUILD_LDFLAGS) cmd/server/cmd.go

build-linux: ## Build for Linux
	go generate ./...
	make build-proto
	GOOS=linux GOARCH=amd64 go build -o myshoes-linux-amd64 -ldflags $(BUILD_LDFLAGS) cmd/server/cmd.go

build-proto: ## Build proto file
	mkdir -p tmp/proto-go
	rm -rf api/proto.go

	protoc -I=api/proto/ --go_out=tmp/proto-go/ --go-grpc_out=tmp/proto-go/ api/proto/**.proto
	mv tmp/proto-go/github.com/whywaita/myshoes/api/proto.go api/
	rm -rf tmp

test: ## Exec test
	go test -v ./...

================================================
FILE: README.md
================================================
# myshoes: Auto scaling self-hosted runner for GitHub Actions

![](./docs/assets/img/myshoes_logo_yoko_colorA.png)

[![awesome-runners](https://img.shields.io/badge/listed%20on-awesome--runners-blue.svg)](https://github.com/jonico/awesome-runners)
[![Go Reference](https://pkg.go.dev/badge/github.com/whywaita/myshoes.svg)](https://pkg.go.dev/github.com/whywaita/myshoes)
[![test](https://github.com/whywaita/myshoes/actions/workflows/test.yaml/badge.svg)](https://github.com/whywaita/myshoes/actions/workflows/test.yaml)
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/whywaita/myshoes)](https://goreportcard.com/report/github.com/whywaita/myshoes)

Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions!

## Features

- Auto-scaling and runner with your cloud-provider
    - your infrastructure (private cloud, homelab...)
        - [LXD](https://linuxcontainers.org): [shoes-lxd](https://github.com/whywaita/myshoes-providers/tree/master/shoes-lxd)
        - [OpenStack](https://www.openstack.org): [shoes-openstack](https://github.com/whywaita/myshoes-providers/tree/master/shoes-openstack)
    - a low-cost instance in public cloud
        - [AWS EC2 Spot Instances](https://aws.amazon.com/ec2/spot): [shoes-aws](https://github.com/whywaita/myshoes-providers/tree/master/shoes-aws)
        - [GCP Preemptible VM instances](https://cloud.google.com/compute/docs/instances/preemptible): shoes-gcp (not yet)
    - using special hardware
        - Graphics Processing Unit (GPU)
        - Field Programmable Gate Array (FPGA)
    - And more in [whywaita/myshoes-providers](https://github.com/whywaita/myshoes-providers)

## Setup (only once)

Please see [Documents](./docs).

## How to contribute

1. Fork it
1. Clone original repository `git clone https://github.com/whywaita/myshoes`
1. Add remote your repository `git remote add your-name https://github.com/${your-name}/myshoes`
1. Create your feature branch `git switch -c my-new-feature`
1. Commit your changes `git commit -am 'Add some feature'`
1. Push to the branch `git push your-name my-new-feature`
1. Create new Pull Request

## Publications

### Talk

- [Development myshoes and Provide Cycloud-hosted runner -- GitHub Actions with your shoes. (en)](https://www.slideshare.net/whywaita/development-myshoes-and-provide-cycloudhosted-runner-github-actions-with-your-shoes)
- [Development OSS CI/CD platform in CyberAgent (ja)](https://www.slideshare.net/whywaita/cyberagent-oss-cicd-myshoes-cicd2021)


================================================
FILE: api/myshoes/README.md
================================================
# myshoes-sdk-go

The Go SDK for myshoes

## Usage

```go
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	
	"github.com/whywaita/myshoes/api/myshoes"
)

func main()  {
	// Set customized HTTP Client
	customHTTPClient := http.DefaultClient
	// Set customized logger
	customLogger := log.New(io.Discard, "", log.LstdFlags)
	
	client, err := myshoes.NewClient("https://example.com", customHTTPClient, customLogger)
	if err != nil {
		// ...
	}
	
	targets, err := client.ListTarget(context.Background())
	if err != nil {
		// ...
	}
	
	fmt.Println(targets)
}
```

================================================
FILE: api/myshoes/client.go
================================================
package myshoes

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"path"
	"strings"
)

// Client is a client for myshoes
type Client struct {
	HTTPClient http.Client
	URL        *url.URL

	UserAgent string
	Logger    *log.Logger
}

const (
	defaultUserAgent = "myshoes-sdk-go"
)

// NewClient create a Client
func NewClient(endpoint string, client *http.Client, logger *log.Logger) (*Client, error) {
	u, err := url.Parse(endpoint)
	if err != nil {
		return nil, fmt.Errorf("failed to parse endpoint: %w", err)
	}

	httpClient := client
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	l := logger
	if l == nil {
		return &Client{
			HTTPClient: *httpClient,
			URL:        u,

			// Default is discard logger
			Logger: log.New(io.Discard, "", log.LstdFlags),
		}, nil
	}

	return &Client{
		HTTPClient: *httpClient,
		URL:        u,

		Logger: l,
	}, nil
}

func (c *Client) newRequest(ctx context.Context, method, spath string, body io.Reader) (*http.Request, error) {
	u := *c.URL
	u.Path = path.Join(c.URL.Path, spath)

	req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
	if err != nil {
		return nil, fmt.Errorf("failed to create a new HTTP request: %w", err)
	}

	ua := c.UserAgent
	if strings.EqualFold(ua, "") {
		ua = defaultUserAgent
	}
	req.Header.Set("User-Agent", ua)

	req.Header.Set("Content-Type", "application/json")

	return req, nil
}

// Error values
var (
	errCreateRequest = "failed to create request: %w"
	errRequest       = "failed to request: %w"
	errDecodeBody    = "failed to decodeBody: %w"
)


================================================
FILE: api/myshoes/http.go
================================================
package myshoes

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/whywaita/myshoes/pkg/web"
)

func decodeBody(resp *http.Response, out interface{}) error {
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to io.ReadAll(resp.Body): %w", err)
	}

	if err := json.Unmarshal(body, out); err != nil {
		return fmt.Errorf("failed to json.Unmarshal() (out: %s): %w", body, err)
	}
	return nil
}

func decodeErrorBody(resp *http.Response) error {
	var e web.ErrorResponse

	if err := decodeBody(resp, &e); err != nil {
		return fmt.Errorf(errDecodeBody, err)
	}

	return fmt.Errorf("%s", e.Error)
}

func (c *Client) request(req *http.Request, out interface{}) error {
	c.Logger.Printf("Do request: %+v", req)
	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to do HTTP request: %w", err)
	}

	switch {
	case resp.StatusCode == http.StatusNoContent:
		return nil
	case resp.StatusCode >= 400:
		return decodeErrorBody(resp)
	}

	if err := decodeBody(resp, out); err != nil {
		return fmt.Errorf(errDecodeBody, err)
	}
	return nil
}


================================================
FILE: api/myshoes/target.go
================================================
package myshoes

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/whywaita/myshoes/pkg/web"
)

// CreateTarget create a target
func (c *Client) CreateTarget(ctx context.Context, param web.TargetCreateParam) (*web.UserTarget, error) {
	spath := "/target"

	jb, err := json.Marshal(param)
	if err != nil {
		return nil, fmt.Errorf("failed to json.Marshal: %w", err)
	}

	req, err := c.newRequest(ctx, http.MethodPost, spath, bytes.NewBuffer(jb))
	if err != nil {
		return nil, fmt.Errorf(errCreateRequest, err)
	}

	var target web.UserTarget
	if err := c.request(req, &target); err != nil {
		return nil, fmt.Errorf(errRequest, err)
	}

	return &target, nil
}

// GetTarget get a target
func (c *Client) GetTarget(ctx context.Context, targetID string) (*web.UserTarget, error) {
	spath := fmt.Sprintf("/target/%s", targetID)

	req, err := c.newRequest(ctx, http.MethodGet, spath, nil)
	if err != nil {
		return nil, fmt.Errorf(errCreateRequest, err)
	}

	var target web.UserTarget
	if err := c.request(req, &target); err != nil {
		return nil, fmt.Errorf(errRequest, err)
	}

	return &target, nil
}

// UpdateTarget update a target
func (c *Client) UpdateTarget(ctx context.Context, targetID string, param web.TargetCreateParam) (*web.UserTarget, error) {
	spath := fmt.Sprintf("/target/%s", targetID)

	jb, err := json.Marshal(param)
	if err != nil {
		return nil, fmt.Errorf("failed to json.Marshal: %w", err)
	}

	req, err := c.newRequest(ctx, http.MethodPost, spath, bytes.NewBuffer(jb))
	if err != nil {
		return nil, fmt.Errorf(errCreateRequest, err)
	}

	var target web.UserTarget
	if err := c.request(req, &target); err != nil {
		return nil, fmt.Errorf(errRequest, err)
	}

	return &target, nil
}

// DeleteTarget delete a target
func (c *Client) DeleteTarget(ctx context.Context, targetID string) error {
	spath := fmt.Sprintf("/target/%s", targetID)

	req, err := c.newRequest(ctx, http.MethodDelete, spath, nil)
	if err != nil {
		return fmt.Errorf(errCreateRequest, err)
	}

	var i interface{} // this endpoint return N/A
	if err := c.request(req, &i); err != nil {
		return fmt.Errorf(errRequest, err)
	}

	return nil
}

// ListTarget get a list of target
func (c *Client) ListTarget(ctx context.Context) ([]web.UserTarget, error) {
	spath := "/target"

	req, err := c.newRequest(ctx, http.MethodGet, spath, nil)
	if err != nil {
		return nil, fmt.Errorf(errCreateRequest, err)
	}

	var targets []web.UserTarget
	if err := c.request(req, &targets); err != nil {
		return nil, fmt.Errorf(errRequest, err)
	}

	return targets, nil
}


================================================
FILE: api/proto/myshoes.proto
================================================
syntax = "proto3";

package whywaita.myshoes;
option go_package = "github.com/whywaita/myshoes/api/proto.go";

service Shoes {
  rpc AddInstance(AddInstanceRequest) returns (AddInstanceResponse) {}
  rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) {}
}

enum ResourceType {
  Unknown = 0;
  Nano = 1;
  Micro = 2;
  Small = 3;
  Medium = 4;
  Large = 5;
  XLarge = 6;
  XLarge2 = 7;
  XLarge3 = 8;
  XLarge4 = 9;
}

message AddInstanceRequest {
  string runner_name = 1;
  string setup_script = 2;
  ResourceType resource_type = 3;
  repeated string labels = 4;
}

message AddInstanceResponse {
  string cloud_id = 1;
  string shoes_type = 2;
  string ip_address = 3;
  ResourceType resource_type = 4;
}

message DeleteInstanceRequest {
  string cloud_id = 1;
  repeated string labels = 2;
}

message DeleteInstanceResponse {}

================================================
FILE: api/proto.go/myshoes.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.36.11
// 	protoc        v5.29.3
// source: myshoes.proto

package proto_go

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
	unsafe "unsafe"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type ResourceType int32

const (
	ResourceType_Unknown ResourceType = 0
	ResourceType_Nano    ResourceType = 1
	ResourceType_Micro   ResourceType = 2
	ResourceType_Small   ResourceType = 3
	ResourceType_Medium  ResourceType = 4
	ResourceType_Large   ResourceType = 5
	ResourceType_XLarge  ResourceType = 6
	ResourceType_XLarge2 ResourceType = 7
	ResourceType_XLarge3 ResourceType = 8
	ResourceType_XLarge4 ResourceType = 9
)

// Enum value maps for ResourceType.
var (
	ResourceType_name = map[int32]string{
		0: "Unknown",
		1: "Nano",
		2: "Micro",
		3: "Small",
		4: "Medium",
		5: "Large",
		6: "XLarge",
		7: "XLarge2",
		8: "XLarge3",
		9: "XLarge4",
	}
	ResourceType_value = map[string]int32{
		"Unknown": 0,
		"Nano":    1,
		"Micro":   2,
		"Small":   3,
		"Medium":  4,
		"Large":   5,
		"XLarge":  6,
		"XLarge2": 7,
		"XLarge3": 8,
		"XLarge4": 9,
	}
)

func (x ResourceType) Enum() *ResourceType {
	p := new(ResourceType)
	*p = x
	return p
}

func (x ResourceType) String() string {
	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}

func (ResourceType) Descriptor() protoreflect.EnumDescriptor {
	return file_myshoes_proto_enumTypes[0].Descriptor()
}

func (ResourceType) Type() protoreflect.EnumType {
	return &file_myshoes_proto_enumTypes[0]
}

func (x ResourceType) Number() protoreflect.EnumNumber {
	return protoreflect.EnumNumber(x)
}

// Deprecated: Use ResourceType.Descriptor instead.
func (ResourceType) EnumDescriptor() ([]byte, []int) {
	return file_myshoes_proto_rawDescGZIP(), []int{0}
}

type AddInstanceRequest struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	RunnerName    string                 `protobuf:"bytes,1,opt,name=runner_name,json=runnerName,proto3" json:"runner_name,omitempty"`
	SetupScript   string                 `protobuf:"bytes,2,opt,name=setup_script,json=setupScript,proto3" json:"setup_script,omitempty"`
	ResourceType  ResourceType           `protobuf:"varint,3,opt,name=resource_type,json=resourceType,proto3,enum=whywaita.myshoes.ResourceType" json:"resource_type,omitempty"`
	Labels        []string               `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *AddInstanceRequest) Reset() {
	*x = AddInstanceRequest{}
	mi := &file_myshoes_proto_msgTypes[0]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *AddInstanceRequest) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*AddInstanceRequest) ProtoMessage() {}

func (x *AddInstanceRequest) ProtoReflect() protoreflect.Message {
	mi := &file_myshoes_proto_msgTypes[0]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use AddInstanceRequest.ProtoReflect.Descriptor instead.
func (*AddInstanceRequest) Descriptor() ([]byte, []int) {
	return file_myshoes_proto_rawDescGZIP(), []int{0}
}

func (x *AddInstanceRequest) GetRunnerName() string {
	if x != nil {
		return x.RunnerName
	}
	return ""
}

func (x *AddInstanceRequest) GetSetupScript() string {
	if x != nil {
		return x.SetupScript
	}
	return ""
}

func (x *AddInstanceRequest) GetResourceType() ResourceType {
	if x != nil {
		return x.ResourceType
	}
	return ResourceType_Unknown
}

func (x *AddInstanceRequest) GetLabels() []string {
	if x != nil {
		return x.Labels
	}
	return nil
}

type AddInstanceResponse struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	CloudId       string                 `protobuf:"bytes,1,opt,name=cloud_id,json=cloudId,proto3" json:"cloud_id,omitempty"`
	ShoesType     string                 `protobuf:"bytes,2,opt,name=shoes_type,json=shoesType,proto3" json:"shoes_type,omitempty"`
	IpAddress     string                 `protobuf:"bytes,3,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"`
	ResourceType  ResourceType           `protobuf:"varint,4,opt,name=resource_type,json=resourceType,proto3,enum=whywaita.myshoes.ResourceType" json:"resource_type,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *AddInstanceResponse) Reset() {
	*x = AddInstanceResponse{}
	mi := &file_myshoes_proto_msgTypes[1]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *AddInstanceResponse) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*AddInstanceResponse) ProtoMessage() {}

func (x *AddInstanceResponse) ProtoReflect() protoreflect.Message {
	mi := &file_myshoes_proto_msgTypes[1]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use AddInstanceResponse.ProtoReflect.Descriptor instead.
func (*AddInstanceResponse) Descriptor() ([]byte, []int) {
	return file_myshoes_proto_rawDescGZIP(), []int{1}
}

func (x *AddInstanceResponse) GetCloudId() string {
	if x != nil {
		return x.CloudId
	}
	return ""
}

func (x *AddInstanceResponse) GetShoesType() string {
	if x != nil {
		return x.ShoesType
	}
	return ""
}

func (x *AddInstanceResponse) GetIpAddress() string {
	if x != nil {
		return x.IpAddress
	}
	return ""
}

func (x *AddInstanceResponse) GetResourceType() ResourceType {
	if x != nil {
		return x.ResourceType
	}
	return ResourceType_Unknown
}

type DeleteInstanceRequest struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	CloudId       string                 `protobuf:"bytes,1,opt,name=cloud_id,json=cloudId,proto3" json:"cloud_id,omitempty"`
	Labels        []string               `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *DeleteInstanceRequest) Reset() {
	*x = DeleteInstanceRequest{}
	mi := &file_myshoes_proto_msgTypes[2]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *DeleteInstanceRequest) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*DeleteInstanceRequest) ProtoMessage() {}

func (x *DeleteInstanceRequest) ProtoReflect() protoreflect.Message {
	mi := &file_myshoes_proto_msgTypes[2]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use DeleteInstanceRequest.ProtoReflect.Descriptor instead.
func (*DeleteInstanceRequest) Descriptor() ([]byte, []int) {
	return file_myshoes_proto_rawDescGZIP(), []int{2}
}

func (x *DeleteInstanceRequest) GetCloudId() string {
	if x != nil {
		return x.CloudId
	}
	return ""
}

func (x *DeleteInstanceRequest) GetLabels() []string {
	if x != nil {
		return x.Labels
	}
	return nil
}

type DeleteInstanceResponse struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *DeleteInstanceResponse) Reset() {
	*x = DeleteInstanceResponse{}
	mi := &file_myshoes_proto_msgTypes[3]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *DeleteInstanceResponse) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*DeleteInstanceResponse) ProtoMessage() {}

func (x *DeleteInstanceResponse) ProtoReflect() protoreflect.Message {
	mi := &file_myshoes_proto_msgTypes[3]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use DeleteInstanceResponse.ProtoReflect.Descriptor instead.
func (*DeleteInstanceResponse) Descriptor() ([]byte, []int) {
	return file_myshoes_proto_rawDescGZIP(), []int{3}
}

var File_myshoes_proto protoreflect.FileDescriptor

const file_myshoes_proto_rawDesc = "" +
	"\n" +
	"\rmyshoes.proto\x12\x10whywaita.myshoes\"\xb5\x01\n" +
	"\x12AddInstanceRequest\x12\x1f\n" +
	"\vrunner_name\x18\x01 \x01(\tR\n" +
	"runnerName\x12!\n" +
	"\fsetup_script\x18\x02 \x01(\tR\vsetupScript\x12C\n" +
	"\rresource_type\x18\x03 \x01(\x0e2\x1e.whywaita.myshoes.ResourceTypeR\fresourceType\x12\x16\n" +
	"\x06labels\x18\x04 \x03(\tR\x06labels\"\xb3\x01\n" +
	"\x13AddInstanceResponse\x12\x19\n" +
	"\bcloud_id\x18\x01 \x01(\tR\acloudId\x12\x1d\n" +
	"\n" +
	"shoes_type\x18\x02 \x01(\tR\tshoesType\x12\x1d\n" +
	"\n" +
	"ip_address\x18\x03 \x01(\tR\tipAddress\x12C\n" +
	"\rresource_type\x18\x04 \x01(\x0e2\x1e.whywaita.myshoes.ResourceTypeR\fresourceType\"J\n" +
	"\x15DeleteInstanceRequest\x12\x19\n" +
	"\bcloud_id\x18\x01 \x01(\tR\acloudId\x12\x16\n" +
	"\x06labels\x18\x02 \x03(\tR\x06labels\"\x18\n" +
	"\x16DeleteInstanceResponse*\x85\x01\n" +
	"\fResourceType\x12\v\n" +
	"\aUnknown\x10\x00\x12\b\n" +
	"\x04Nano\x10\x01\x12\t\n" +
	"\x05Micro\x10\x02\x12\t\n" +
	"\x05Small\x10\x03\x12\n" +
	"\n" +
	"\x06Medium\x10\x04\x12\t\n" +
	"\x05Large\x10\x05\x12\n" +
	"\n" +
	"\x06XLarge\x10\x06\x12\v\n" +
	"\aXLarge2\x10\a\x12\v\n" +
	"\aXLarge3\x10\b\x12\v\n" +
	"\aXLarge4\x10\t2\xcc\x01\n" +
	"\x05Shoes\x12\\\n" +
	"\vAddInstance\x12$.whywaita.myshoes.AddInstanceRequest\x1a%.whywaita.myshoes.AddInstanceResponse\"\x00\x12e\n" +
	"\x0eDeleteInstance\x12'.whywaita.myshoes.DeleteInstanceRequest\x1a(.whywaita.myshoes.DeleteInstanceResponse\"\x00B*Z(github.com/whywaita/myshoes/api/proto.gob\x06proto3"

var (
	file_myshoes_proto_rawDescOnce sync.Once
	file_myshoes_proto_rawDescData []byte
)

func file_myshoes_proto_rawDescGZIP() []byte {
	file_myshoes_proto_rawDescOnce.Do(func() {
		file_myshoes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_myshoes_proto_rawDesc), len(file_myshoes_proto_rawDesc)))
	})
	return file_myshoes_proto_rawDescData
}

var file_myshoes_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_myshoes_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_myshoes_proto_goTypes = []any{
	(ResourceType)(0),              // 0: whywaita.myshoes.ResourceType
	(*AddInstanceRequest)(nil),     // 1: whywaita.myshoes.AddInstanceRequest
	(*AddInstanceResponse)(nil),    // 2: whywaita.myshoes.AddInstanceResponse
	(*DeleteInstanceRequest)(nil),  // 3: whywaita.myshoes.DeleteInstanceRequest
	(*DeleteInstanceResponse)(nil), // 4: whywaita.myshoes.DeleteInstanceResponse
}
var file_myshoes_proto_depIdxs = []int32{
	0, // 0: whywaita.myshoes.AddInstanceRequest.resource_type:type_name -> whywaita.myshoes.ResourceType
	0, // 1: whywaita.myshoes.AddInstanceResponse.resource_type:type_name -> whywaita.myshoes.ResourceType
	1, // 2: whywaita.myshoes.Shoes.AddInstance:input_type -> whywaita.myshoes.AddInstanceRequest
	3, // 3: whywaita.myshoes.Shoes.DeleteInstance:input_type -> whywaita.myshoes.DeleteInstanceRequest
	2, // 4: whywaita.myshoes.Shoes.AddInstance:output_type -> whywaita.myshoes.AddInstanceResponse
	4, // 5: whywaita.myshoes.Shoes.DeleteInstance:output_type -> whywaita.myshoes.DeleteInstanceResponse
	4, // [4:6] is the sub-list for method output_type
	2, // [2:4] is the sub-list for method input_type
	2, // [2:2] is the sub-list for extension type_name
	2, // [2:2] is the sub-list for extension extendee
	0, // [0:2] is the sub-list for field type_name
}

func init() { file_myshoes_proto_init() }
func file_myshoes_proto_init() {
	if File_myshoes_proto != nil {
		return
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: unsafe.Slice(unsafe.StringData(file_myshoes_proto_rawDesc), len(file_myshoes_proto_rawDesc)),
			NumEnums:      1,
			NumMessages:   4,
			NumExtensions: 0,
			NumServices:   1,
		},
		GoTypes:           file_myshoes_proto_goTypes,
		DependencyIndexes: file_myshoes_proto_depIdxs,
		EnumInfos:         file_myshoes_proto_enumTypes,
		MessageInfos:      file_myshoes_proto_msgTypes,
	}.Build()
	File_myshoes_proto = out.File
	file_myshoes_proto_goTypes = nil
	file_myshoes_proto_depIdxs = nil
}


================================================
FILE: api/proto.go/myshoes_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.0
// - protoc             v5.29.3
// source: myshoes.proto

package proto_go

import (
	context "context"
	grpc "google.golang.org/grpc"
	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9

const (
	Shoes_AddInstance_FullMethodName    = "/whywaita.myshoes.Shoes/AddInstance"
	Shoes_DeleteInstance_FullMethodName = "/whywaita.myshoes.Shoes/DeleteInstance"
)

// ShoesClient is the client API for Shoes service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ShoesClient interface {
	AddInstance(ctx context.Context, in *AddInstanceRequest, opts ...grpc.CallOption) (*AddInstanceResponse, error)
	DeleteInstance(ctx context.Context, in *DeleteInstanceRequest, opts ...grpc.CallOption) (*DeleteInstanceResponse, error)
}

type shoesClient struct {
	cc grpc.ClientConnInterface
}

func NewShoesClient(cc grpc.ClientConnInterface) ShoesClient {
	return &shoesClient{cc}
}

func (c *shoesClient) AddInstance(ctx context.Context, in *AddInstanceRequest, opts ...grpc.CallOption) (*AddInstanceResponse, error) {
	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
	out := new(AddInstanceResponse)
	err := c.cc.Invoke(ctx, Shoes_AddInstance_FullMethodName, in, out, cOpts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

func (c *shoesClient) DeleteInstance(ctx context.Context, in *DeleteInstanceRequest, opts ...grpc.CallOption) (*DeleteInstanceResponse, error) {
	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
	out := new(DeleteInstanceResponse)
	err := c.cc.Invoke(ctx, Shoes_DeleteInstance_FullMethodName, in, out, cOpts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// ShoesServer is the server API for Shoes service.
// All implementations must embed UnimplementedShoesServer
// for forward compatibility.
type ShoesServer interface {
	AddInstance(context.Context, *AddInstanceRequest) (*AddInstanceResponse, error)
	DeleteInstance(context.Context, *DeleteInstanceRequest) (*DeleteInstanceResponse, error)
	mustEmbedUnimplementedShoesServer()
}

// UnimplementedShoesServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedShoesServer struct{}

func (UnimplementedShoesServer) AddInstance(context.Context, *AddInstanceRequest) (*AddInstanceResponse, error) {
	return nil, status.Error(codes.Unimplemented, "method AddInstance not implemented")
}
func (UnimplementedShoesServer) DeleteInstance(context.Context, *DeleteInstanceRequest) (*DeleteInstanceResponse, error) {
	return nil, status.Error(codes.Unimplemented, "method DeleteInstance not implemented")
}
func (UnimplementedShoesServer) mustEmbedUnimplementedShoesServer() {}
func (UnimplementedShoesServer) testEmbeddedByValue()               {}

// UnsafeShoesServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ShoesServer will
// result in compilation errors.
type UnsafeShoesServer interface {
	mustEmbedUnimplementedShoesServer()
}

func RegisterShoesServer(s grpc.ServiceRegistrar, srv ShoesServer) {
	// If the following call panics, it indicates UnimplementedShoesServer was
	// embedded by pointer and is nil.  This will cause panics if an
	// unimplemented method is ever invoked, so we test this at initialization
	// time to prevent it from happening at runtime later due to I/O.
	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
		t.testEmbeddedByValue()
	}
	s.RegisterService(&Shoes_ServiceDesc, srv)
}

func _Shoes_AddInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(AddInstanceRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(ShoesServer).AddInstance(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: Shoes_AddInstance_FullMethodName,
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(ShoesServer).AddInstance(ctx, req.(*AddInstanceRequest))
	}
	return interceptor(ctx, in, info, handler)
}

func _Shoes_DeleteInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(DeleteInstanceRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(ShoesServer).DeleteInstance(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: Shoes_DeleteInstance_FullMethodName,
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(ShoesServer).DeleteInstance(ctx, req.(*DeleteInstanceRequest))
	}
	return interceptor(ctx, in, info, handler)
}

// Shoes_ServiceDesc is the grpc.ServiceDesc for Shoes service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Shoes_ServiceDesc = grpc.ServiceDesc{
	ServiceName: "whywaita.myshoes.Shoes",
	HandlerType: (*ShoesServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "AddInstance",
			Handler:    _Shoes_AddInstance_Handler,
		},
		{
			MethodName: "DeleteInstance",
			Handler:    _Shoes_DeleteInstance_Handler,
		},
	},
	Streams:  []grpc.StreamDesc{},
	Metadata: "myshoes.proto",
}


================================================
FILE: cmd/server/cmd.go
================================================
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"runtime"
	"strings"
	"time"

	"github.com/whywaita/myshoes/pkg/config"
	"github.com/whywaita/myshoes/pkg/datastore"
	"github.com/whywaita/myshoes/pkg/datastore/mysql"
	"github.com/whywaita/myshoes/pkg/gh"
	"github.com/whywaita/myshoes/pkg/logger"
	"github.com/whywaita/myshoes/pkg/runner"
	"github.com/whywaita/myshoes/pkg/starter"
	"github.com/whywaita/myshoes/pkg/starter/safety/unlimited"
	"github.com/whywaita/myshoes/pkg/web"

	"golang.org/x/sync/errgroup"
)

func init() {
	config.Load()
	mysqlURL := config.LoadMySQLURL()
	config.Config.MySQLDSN = mysqlURL

	if err := gh.InitializeCache(config.Config.GitHub.AppID, config.Config.GitHub.PEMByte); err != nil {
		log.Panicf("failed to create a cache: %+v", err)
	}
}

func main() {
	runtime.SetBlockProfileRate(1)
	runtime.SetMutexProfileFraction(1)
	go func() {
		log.Fatal(http.ListenAndServe("localhost:6060", nil))
	}()

	myshoes, err := newShoes()
	if err != nil {
		log.Fatalln(err)
	}

	if err := myshoes.Run(); err != nil {
		log.Fatalln(err)
	}
}

type myShoes struct {
	ds    datastore.Datastore
	start *starter.Starter
	run   *runner.Manager
}

// newShoes create myshoes.
func newShoes() (*myShoes, error) {
	notifyEnqueueCh := make(chan struct{}, 1)

	ds, err := mysql.New(config.Config.MySQLDSN, notifyEnqueueCh)
	if err != nil {
		return nil, fmt.Errorf("failed to mysql.New: %w", err)
	}

	unlimit := unlimited.Unlimited{}
	s := starter.New(ds, unlimit, config.Config.RunnerVersion, notifyEnqueueCh)

	manager := runner.New(ds, config.Config.RunnerVersion)

	return &myShoes{
		ds:    ds,
		start: s,
		run:   manager,
	}, nil
}

// Run start services.
func (m *myShoes) Run() error {
	eg, ctx := errgroup.WithContext(context.Background())

	for {
		logger.Logf(false, "start getting lock...")
		isLocked, err := m.ds.IsLocked(ctx)
		if err != nil {
			return fmt.Errorf("failed to check lock: %w", err)
		}

		if strings.EqualFold(isLocked, datastore.IsNotLocked) {
			if err := m.ds.GetLock(ctx); err != nil {
				return fmt.Errorf("failed to get lock: %w", err)
			}

			logger.Logf(false, "get lock successfully!")
			break
		}

		time.Sleep(time.Second)
	}

	eg.Go(func() error {
		if err := web.Serve(ctx, m.ds); err != nil {
			logger.Logf(false, "failed to web.Serve: %+v", err)
			return fmt.Errorf("failed to serve: %w", err)
		}
		return nil
	})
	eg.Go(func() error {
		if err := m.start.Loop(ctx); err != nil {
			logger.Logf(false, "failed to starter manager: %+v", err)
			return fmt.Errorf("failed to starter loop: %w", err)
		}
		return nil
	})
	eg.Go(func() error {
		if err := m.run.Loop(ctx); err != nil {
			logger.Logf(false, "failed to runner manager: %+v", err)
			return fmt.Errorf("failed to runner loop: %w", err)
		}
		return nil
	})

	if err := eg.Wait(); err != nil {
		return fmt.Errorf("failed to wait errgroup: %w", err)
	}

	return nil
}


================================================
FILE: cmd/shoes-tester/main.go
================================================
package main

import (
	"context"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"github.com/hashicorp/go-plugin"
	"github.com/whywaita/myshoes/pkg/config"
	"github.com/whywaita/myshoes/pkg/datastore"
	"github.com/whywaita/myshoes/pkg/gh"
	"github.com/whywaita/myshoes/pkg/shoes"
	"github.com/whywaita/myshoes/pkg/starter"
)

func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(1)
	}

	subcommand := os.Args[1]
	switch subcommand {
	case "add":
		if err := runAdd(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}
	case "delete":
		if err := runDelete(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}
	default:
		fmt.Fprintf(os.Stderr, "Unknown subcommand: %s\n", subcommand)
		printUsage()
		os.Exit(1)
	}
}

func printUsage() {
	fmt.Fprintf(os.Stderr, `Usage: shoes-tester <command> [options]

Commands:
  add       Add an instance
  delete    Delete an instance

Run 'shoes-tester <command> --help' for more information on a command.
`)
}

type addFlags struct {
	pluginPath           string
	runnerName           string
	resourceType         string
	labels               string
	setupScript          string
	generateScript       bool
	scope                string
	githubAppID          string
	githubPrivateKeyPath string
	runnerVersion        string
	runnerUser           string
	runnerBaseDirectory  string
	githubURL            string
	jsonOutput           bool
}

type deleteFlags struct {
	pluginPath string
	cloudID    string
	labels     string
	jsonOutput bool
}

func runAdd(args []string) error {
	fs := flag.NewFlagSet("add", flag.ExitOnError)
	flags := &addFlags{}

	fs.StringVar(&flags.pluginPath, "plugin", "", "Path to shoes-provider binary (required)")
	fs.StringVar(&flags.runnerName, "runner-name", "", "Runner name (required)")
	fs.StringVar(&flags.resourceType, "resource-type", "nano", "Resource type (nano|micro|small|medium|large|xlarge|2xlarge|3xlarge|4xlarge)")
	fs.StringVar(&flags.labels, "labels", "", "Comma-separated labels")
	fs.StringVar(&flags.setupScript, "setup-script", "", "Setup script (simple mode)")
	fs.BoolVar(&flags.generateScript, "generate-script", false, "Generate setup script (script generation mode)")
	fs.StringVar(&flags.scope, "scope", "", "Repository (owner/repo) or Organization (script generation mode)")
	fs.StringVar(&flags.githubAppID, "github-app-id", os.Getenv("GITHUB_APP_ID"), "GitHub App ID (script generation mode)")
	fs.StringVar(&flags.githubPrivateKeyPath, "github-private-key-path", os.Getenv("GITHUB_PRIVATE_KEY_PATH"), "GitHub App private key path (script generation mode)")
	fs.StringVar(&flags.runnerVersion, "runner-version", "latest", "Runner version (script generation mode)")
	fs.StringVar(&flags.runnerUser, "runner-user", "runner", "Runner user (script generation mode)")
	fs.StringVar(&flags.runnerBaseDirectory, "runner-base-directory", "/tmp", "Runner base directory (script generation mode)")
	fs.StringVar(&flags.githubURL, "github-url", "", "GitHub Enterprise Server URL (script generation mode)")
	fs.BoolVar(&flags.jsonOutput, "json", false, "Output in JSON format")

	fs.Parse(args)

	if flags.pluginPath == "" {
		return fmt.Errorf("--plugin is required")
	}
	if flags.runnerName == "" {
		return fmt.Errorf("--runner-name is required")
	}

	if flags.generateScript {
		if flags.scope == "" {
			return fmt.Errorf("--scope is required for script generation mode")
		}
		if flags.githubAppID == "" {
			return fmt.Errorf("--github-app-id is required for script generation mode")
		}
		if flags.githubPrivateKeyPath == "" {
			return fmt.Errorf("--github-private-key-path is required for script generation mode")
		}
	}

	ctx := context.Background()

	var setupScript string
	if flags.generateScript {
		script, err := generateSetupScript(ctx, flags)
		if err != nil {
			return fmt.Errorf("failed to generate setup script: %w", err)
		}
		setupScript = script
	} else {
		setupScript = flags.setupScript
	}

	resourceType := datastore.UnmarshalResourceTypeString(flags.resourceType)
	if resourceType == datastore.ResourceTypeUnknown && flags.resourceType != "unknown" {
		return fmt.Errorf("invalid resource type: %s", flags.resourceType)
	}

	labels := parseLabels(flags.labels)

	client, teardown, err := getClientWithPath(flags.pluginPath)
	if err != nil {
		return fmt.Errorf("failed to get plugin client: %w", err)
	}
	defer teardown()

	cloudID, ipAddress, shoesType, actualResourceType, err := client.AddInstance(ctx, flags.runnerName, setupScript, resourceType, labels)
	if err != nil {
		return fmt.Errorf("failed to add instance: %w", err)
	}

	if flags.jsonOutput {
		output := map[string]string{
			"cloud_id":      cloudID,
			"ip_address":    ipAddress,
			"shoes_type":    shoesType,
			"resource_type": actualResourceType.String(),
		}
		data, err := json.Marshal(output)
		if err != nil {
			return fmt.Errorf("failed to marshal JSON: %w", err)
		}
		fmt.Println(string(data))
	} else {
		fmt.Printf("AddInstance succeeded:\n")
		fmt.Printf("  Cloud ID:      %s\n", cloudID)
		fmt.Printf("  Shoes Type:    %s\n", shoesType)
		fmt.Printf("  IP Address:    %s\n", ipAddress)
		fmt.Printf("  Resource Type: %s\n", actualResourceType.String())
	}

	return nil
}

func runDelete(args []string) error {
	fs := flag.NewFlagSet("delete", flag.ExitOnError)
	flags := &deleteFlags{}

	fs.StringVar(&flags.pluginPath, "plugin", "", "Path to shoes-provider binary (required)")
	fs.StringVar(&flags.cloudID, "cloud-id", "", "Cloud ID (required)")
	fs.StringVar(&flags.labels, "labels", "", "Comma-separated labels")
	fs.BoolVar(&flags.jsonOutput, "json", false, "Output in JSON format")

	fs.Parse(args)

	if flags.pluginPath == "" {
		return fmt.Errorf("--plugin is required")
	}
	if flags.cloudID == "" {
		return fmt.Errorf("--cloud-id is required")
	}

	ctx := context.Background()

	labels := parseLabels(flags.labels)

	client, teardown, err := getClientWithPath(flags.pluginPath)
	if err != nil {
		return fmt.Errorf("failed to get plugin client: %w", err)
	}
	defer teardown()

	if err := client.DeleteInstance(ctx, flags.cloudID, labels); err != nil {
		return fmt.Errorf("failed to delete instance: %w", err)
	}

	if flags.jsonOutput {
		output := map[string]string{
			"status": "success",
		}
		data, err := json.Marshal(output)
		if err != nil {
			return fmt.Errorf("failed to marshal JSON: %w", err)
		}
		fmt.Println(string(data))
	} else {
		fmt.Printf("DeleteInstance succeeded\n")
	}

	return nil
}

func getClientWithPath(pluginPath string) (shoes.Client, func(), error) {
	handshake := plugin.HandshakeConfig{
		ProtocolVersion:  1,
		MagicCookieKey:   "SHOES_PLUGIN_MAGIC_COOKIE",
		MagicCookieValue: "are_you_a_shoes?",
	}
	pluginMap := map[string]plugin.Plugin{
		"shoes_grpc": &shoes.Plugin{},
	}

	client := plugin.NewClient(&plugin.ClientConfig{
		HandshakeConfig:  handshake,
		Plugins:          pluginMap,
		Cmd:              exec.Command(pluginPath),
		Managed:          true,
		Stderr:           os.Stderr,
		SyncStdout:       os.Stdout,
		SyncStderr:       os.Stderr,
		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
	})

	rpcClient, err := client.Client()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get shoes client: %w", err)
	}

	raw, err := rpcClient.Dispense("shoes_grpc")
	if err != nil {
		return nil, nil, fmt.Errorf("failed to shoes client instance: %w", err)
	}

	return raw.(shoes.Client), client.Kill, nil
}

func generateSetupScript(ctx context.Context, flags *addFlags) (string, error) {
	keyBytes, err := os.ReadFile(flags.githubPrivateKeyPath)
	if err != nil {
		return "", fmt.Errorf("failed to read private key: %w", err)
	}

	appID, err := strconv.ParseInt(flags.githubAppID, 10, 64)
	if err != nil {
		return "", fmt.Errorf("failed to parse github-app-id: %w", err)
	}

	block, _ := pem.Decode(keyBytes)
	if block == nil {
		return "", fmt.Errorf("failed to decode PEM block from private key")
	}

	privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		return "", fmt.Errorf("failed to parse private key: %w", err)
	}

	config.Config.GitHub.AppID = appID
	config.Config.GitHub.PEMByte = keyBytes
	config.Config.GitHub.PEM = privateKey

	if flags.githubURL == "" {
		config.Config.GitHubURL = "https://github.com"
	} else {
		config.Config.GitHubURL = flags.githubURL
	}

	config.Config.RunnerUser = flags.runnerUser
	config.Config.RunnerBaseDirectory = flags.runnerBaseDirectory

	if err := gh.InitializeCache(appID, keyBytes); err != nil {
		return "", fmt.Errorf("failed to initialize GitHub client cache: %w", err)
	}

	s := starter.New(nil, nil, flags.runnerVersion, nil)
	return s.GetSetupScript(ctx, flags.scope, flags.runnerName)
}

func parseLabels(labels string) []string {
	if labels == "" {
		return []string{}
	}
	parts := strings.Split(labels, ",")
	result := make([]string, 0, len(parts))
	for _, p := range parts {
		trimmed := strings.TrimSpace(p)
		if trimmed != "" {
			result = append(result, trimmed)
		}
	}
	return result
}


================================================
FILE: docs/01_01_for_admin_setup.md
================================================
# Setup myshoes daemon

## Goal

- Start myshoes daemon

## Prepare

- The network connectivity to myshoes server.
  - The webhook endpoint from github.com **OR** your GitHub Enterprise Server (`POST /github/events`).
  - REST API from your workspace (`GET, POST, DELETE /target`).
- You decide platform for runner and shoes-provider
  - The official shoes-provider topic is [myshoes-provider](https://github.com/search?q=topic%3Amyshoes-provider).
  - You can implement and use your private shoes-provider. Please check [how-to-develop-shoes.md](./03_how-to-develop-shoes.md).

## Word definition

- `your_shoes_host`: The endpoint of serving myshoes.
  - e.g.) `https://myshoes.example.com`

## Setup

Please prepare a few things first.

### Machine image for runner

- Virtual Machine Image on your cloud provider.
  - installed a some commands.
    - required: curl (1)
    - optional: jq (1), docker (1)
      - optional, but **STRONG RECOMMEND INSTALLING BEFORE** (please read known issue)
  - put latest runner tar.gz to `/usr/local/etc` [optional]
    - optional, but **STRONG RECOMMEND INSTALLING BEFORE** (please read known issue)

For example is [here](https://github.com/whywaita/myshoes-providers/tree/master/shoes-lxd/images). (packer file)

### Create GitHub Apps

#### Configure values

- GitHub App Name: any text
- Homepage URL: any text
  
##### Webhook
- Webhook URL: `${your_shoes_host}/github/events`
- Webhook secret: any text

##### Repository permissions

- Actions: Read-only
- Administration: Read & write
- Checks: Read-only

##### Organization permissions

- Self-hosted runners: Read & write
  
##### Subscribe to events

- Check `Workflow job`

### Download private key

- download from GitHub or upload private key from your machine.

### Running

```bash
$ make build
$ ./myshoes
```

A config variables can set from environment values.

- `PORT`
  - default: 8080
  - Listen port for myshoes.
- GitHub Apps information
  - required
  - `GITHUB_APP_ID`
  - `GITHUB_APP_SECRET` (if you set `Webhook secret` for your GitHub App)
  - `GITHUB_PRIVATE_KEY_BASE64`
    - base64 encoded private key from GitHub Apps
    - `$ cat privatekey.pem | base64 -w 0`
- `MYSQL_URL`
  - required
  - DataSource Name, ex) `username:password@tcp(localhost:3306)/myshoes`
  - if `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE` all are set, this env value are ignored.
- `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE`
  - optional
  - If all environment variables are set, mysql_url is constructed and loaded in the following way, then `MYSQL_URL` env will be ignored.
  - example) `${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DATABASE}`
- `PLUGIN`
  - required
  - set path of myshoes-provider binary.
  - example) `./shoes-mock` `https://example.com/shoes-mock` `https://github.com/whywaita/myshoes-providers/releases/download/v0.1.0/shoes-lxd-linux-amd64`
- `PLUGIN_OUTPUT`
  - default: `.`
  - set path of directory that contains myshoes-provider binary.
- `GITHUB_URL`
  - default: `https://github.com`
  - The URL of GitHub Enterprise Server.
  - Please contain schema.
- `RUNNER_VERSION`
  - default: `latest`
    - Use the latest version in starting job
  - The version of `actions/runner`
  - example) `v2.302.1`, `latest`
- `RUNNER_USER`
  - default: `runner`
  - set linux username that executes runner. you need to set exist user.
    - DO NOT set root. It can't run GitHub Actions runner in root permission.
    - Example: `ubuntu`
- `PROVIDE_DOCKER_HUB_METRICS`
  - default: `false`
  - set `true` if you want to provide rate-limit metrics for Docker Hub.
  - If you're not anonymous user, you need to set `DOCKER_HUB_USERNAME` and `DOCKER_HUB_PASSWORD`.
- `DOCKER_HUB_USERNAME`
  - default: `` (empty)
  - set Docker Hub username for pulling Docker image. (Use for provide rate-limit metrics)
- `DOCKER_HUB_PASSWORD`
  - default: `` (empty)
  - set Docker Hub password for pulling Docker image. (Use for provide rate-limit metrics)


For tuning values

- `DEBUG`
  - default: false
  - show debugging log
- `STRICT`
  - default: true
  - set strict mode
- `MODE_WEBHOOK_TYPE`
  - default: `workflow_job` (use receive `workflow_job` event)
  - Set type of webhook from GitHub
  - option: `check_run`
- `MAX_CONNECTIONS_TO_BACKEND`
  - default: 50
  - The number of max connections to shoes-provider
- `MAX_CONCURRENCY_DELETING`
  - default: 1
  - The number of max concurrency of deleting

and more some env values from [shoes provider](https://github.com/search?q=topic%3Amyshoes-provider).


================================================
FILE: docs/01_02_for_admin_tips.md
================================================
# Tips for myshoes admin

## Job management hooks for self-hosted runners

You can use job management hooks for self-hosted runners.
Please set script file to your runner image.

- `ACTIONS_RUNNER_HOOK_JOB_STARTED`: `/myshoes-actions-runner-hook-job-started.sh`
- `ACTIONS_RUNNER_HOOK_JOB_COMPLETED`: `/myshoes-actions-runner-hook-job-completed.sh`

================================================
FILE: docs/02_01_for_user_setup.md
================================================
# Setup (only once)

## Goal

- Start provision runner

## Prepare

- Get GitHub Apps's Public page from myshoes admin.
  - e.g.) `https://github.com/apps/<GitHub Apps name>` or `<GHES url>/github-apps/<GitHub Apps name>`
- Get Endpoint for myshoes from myshoes admin.
  - e.g.) `<your_shoes_host>/target`

## Repository or Organization setup

### Install GitHub Apps

Please open GitHub Apps's Public page and install GitHub Apps to Organization or repository.

![](./assets/img/02_01_githubapps_publicpage.png)

![](./assets/img/02_01_githubapps_installpage.png)

### Register target to myshoes

you need to register a target that repository or organization.

- `scope`: set target scope for an auto-scaling runner.
  - Repository example: `octocat/hello-worlds`
  - Organization example: `octocat`
- `resource_type`: set instance size for a runner.
  - We will describe later.
  - Please teach it from myshoes admin.

Example (create a target):

```bash
$ curl -XPOST -d '{"scope": "octocat/hello-world", "resource_type": "micro"}' ${your_shoes_host}/target
```

You can check registered targets.

```bash
curl -XGET ${your_shoes_host}/target | jq .
[
  {
    "id": "477f6073-90d1-47d8-958f-4707cea61e8d",
    "scope": "octocat",
    "token_expired_at": "2006-01-02T15:04:05Z",
    "resource_type": "micro",
    "provider_url": "",
    "status": "active",
    "status_description": "",
    "created_at": "2006-01-02T15:04:05Z",
    "updated_at": "2006-01-02T15:04:05Z"
  }
]
```

#### Switch `resource_type`

You can set `resource_type` in target. So myshoes switch size of instance.

For example,

- In organization scope (`octocat`), want to set small size runner as a `nano`.
- But specific repository (`octocat/huge-repository`), want to set big size runner as a `4xlarge`.

So please configure it.

```bash
$ curl -XPOST -d '{"scope": "octocat", "resource_type": "nano"}' ${your_shoes_host}/target
$ curl -XPOST -d '{"scope": "octocat/huge-repository", "resource_type": "4xlarge"}' ${your_shoes_host}/target

$ curl -XGET ${your_shoes_host}/target | jq .
[
  {
    "id": "477f6073-90d1-47d8-958f-4707cea61e8d",
    "scope": "octocat",
    "token_expired_at": "2006-01-02T15:04:05Z",
    "resource_type": "nano",
    "provider_url": "",
    "status": "active",
    "status_description": "",
    "created_at": "2006-01-02T15:04:05Z",
    "updated_at": "2006-01-02T15:04:05Z"
  },
  {
    "id": "3775e3b6-08e0-4abc-830d-fd5325397de0",
    "scope": "octocat/huge-repository",
    "token_expired_at": "2006-01-02T15:04:05Z",
    "resource_type": "4xlarge",
    "provider_url": "",
    "status": "active",
    "status_description": "",
    "created_at": "2006-01-02T15:04:05Z",
    "updated_at": "2006-01-02T15:04:05Z"
  }
]
```

In this configuration, myshoes will create under it.

- In `octocat/normal-repository`, will create `nano`
- In `octocat/normal-repository2`, will create `nano`
- In `octocat/huge-repository`, will create `4xlarge`

### Create an offline runner (only use `check_run` mode)

GitHub Actions need offline runner if queueing job.
Please create an offline runner in the target repository.

https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners/adding-self-hosted-runners

Please delete a runner after registered.

After that, You can use [cycloud-io/refresh-runner-action](https://github.com/cycloud-io/refresh-runner-action) for automation.

### Let's go using your shoes!

Let's execute your jobs! :runner::runner::runner:


================================================
FILE: docs/03_how-to-develop-shoes.md
================================================
# How to develop shoes provider

## TL;DR

- implement to gRPC server
    - `shoes`, `health`, `stdio`
- define resource type in your shoes provider's flavor.

## gRPC server

shoes provider use [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin).
you need to register three Service.

if you use a golang in development, you can use `pkg/pluginutils/setup.go`.

please check `plugins/shoes-mock`. There are mock shoes provider.

### shoes server

`shoes` is gRPC server. you need to implement two funtion.

- `AddInstance`
- `DeleteInstance`

please check `api/proto/myshoes.proto`.

### health

`health` is [grpc-ecosystem/grpc-health-probe](https://github.com/grpc-ecosystem/grpc-health-probe).

### stdio

`stdio` is standard I/O service.

this service communicate plugin binary's standard I/O. 

## Resource type

myshoes defined some machine type. you need to map machine spec for your resource type.

- nano
- micro
- small
- medium
- large
- xlarge
- 2xlarge
- 3xlarge
- 4xlarge

## Testing shoes provider

`shoes-tester` is a CLI tool for testing shoes provider without running myshoes server.

### Build

```bash
go build -o shoes-tester ./cmd/shoes-tester
```

### Usage

#### Add instance

Simple mode (with setup script):

```bash
./shoes-tester add \
  --plugin ./path/to/your-shoes-provider \
  --runner-name test-runner \
  --resource-type nano \
  --labels "label1,label2" \
  --setup-script "#!/bin/bash\necho 'setup'"
```

Script generation mode (generate setup script automatically):

```bash
./shoes-tester add \
  --plugin ./path/to/your-shoes-provider \
  --runner-name test-runner \
  --resource-type nano \
  --generate-script \
  --scope owner/repo \
  --github-app-id 123456 \
  --github-private-key-path /path/to/key.pem \
  --runner-version latest
```

#### Delete instance

```bash
./shoes-tester delete \
  --plugin ./path/to/your-shoes-provider \
  --cloud-id your-cloud-id \
  --labels "label1,label2"
```

#### Options

Add command:
- `--plugin`: Path to shoes-provider binary (required)
- `--runner-name`: Runner name (required)
- `--resource-type`: Resource type (default: nano)
- `--labels`: Comma-separated labels
- `--setup-script`: Setup script (simple mode)
- `--generate-script`: Generate setup script automatically (script generation mode)
- `--scope`: Repository (owner/repo) or Organization (script generation mode)
- `--github-app-id`: GitHub App ID (script generation mode)
- `--github-private-key-path`: GitHub App private key path (script generation mode)
- `--runner-version`: Runner version (default: latest, script generation mode)
- `--runner-user`: Runner user (default: runner, script generation mode)
- `--runner-base-directory`: Runner base directory (default: /tmp, script generation mode)
- `--github-url`: GitHub Enterprise Server URL (script generation mode)
- `--json`: Output in JSON format

Delete command:
- `--plugin`: Path to shoes-provider binary (required)
- `--cloud-id`: Cloud ID (required)
- `--labels`: Comma-separated labels
- `--json`: Output in JSON format

================================================
FILE: docs/assets/myshoes.service
================================================
[Unit]
Description=myshoes is Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions
After=network.target

[Service]
User=root
EnvironmentFile=/etc/default/myshoes
ExecStart=/usr/local/bin/myshoes
Restart=always

[Install]
WantedBy=multi-user.target

================================================
FILE: go.mod
================================================
module github.com/whywaita/myshoes

go 1.25

require (
	github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
	github.com/go-sql-driver/mysql v1.9.3
	github.com/golang-jwt/jwt/v4 v4.5.2
	github.com/google/go-cmp v0.7.0
	github.com/google/go-github/v80 v80.0.0
	github.com/hashicorp/go-plugin v1.7.0
	github.com/hashicorp/go-version v1.8.0
	github.com/jmoiron/sqlx v1.4.0
	github.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f
	github.com/ory/dockertest/v3 v3.12.0
	github.com/patrickmn/go-cache v2.1.0+incompatible
	github.com/prometheus/client_golang v1.23.2
	github.com/r3labs/diff/v2 v2.15.1
	github.com/satori/go.uuid v1.2.0
	goji.io v2.0.2+incompatible
	golang.org/x/oauth2 v0.32.0
	golang.org/x/sync v0.18.0
	google.golang.org/grpc v1.77.0
	google.golang.org/protobuf v1.36.10
)

require (
	dario.cat/mergo v1.0.2 // indirect
	filippo.io/edwards25519 v1.1.0 // indirect
	github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
	github.com/Microsoft/go-winio v0.6.2 // indirect
	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/containerd/continuity v0.4.5 // indirect
	github.com/containerd/errdefs v1.0.0 // indirect
	github.com/containerd/errdefs/pkg v0.3.0 // indirect
	github.com/distribution/reference v0.6.0 // indirect
	github.com/docker/cli v29.1.2+incompatible // indirect
	github.com/docker/go-connections v0.6.0 // indirect
	github.com/docker/go-units v0.5.0 // indirect
	github.com/fatih/color v1.18.0 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/google/go-github/v75 v75.0.0 // indirect
	github.com/google/go-querystring v1.1.0 // indirect
	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
	github.com/hashicorp/go-hclog v1.6.3 // indirect
	github.com/hashicorp/yamux v0.1.2 // indirect
	github.com/kylelemons/godebug v1.1.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/moby/docker-image-spec v1.3.1 // indirect
	github.com/moby/moby/api v1.52.0 // indirect
	github.com/moby/moby/client v0.2.1 // indirect
	github.com/moby/sys/user v0.4.0 // indirect
	github.com/moby/term v0.5.2 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/oklog/run v1.2.0 // indirect
	github.com/opencontainers/go-digest v1.0.0 // indirect
	github.com/opencontainers/image-spec v1.1.1 // indirect
	github.com/opencontainers/runc v1.2.3 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/common v0.66.1 // indirect
	github.com/prometheus/procfs v0.16.1 // indirect
	github.com/sirupsen/logrus v1.9.3 // indirect
	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
	go.opentelemetry.io/otel v1.38.0 // indirect
	go.opentelemetry.io/otel/metric v1.38.0 // indirect
	go.opentelemetry.io/otel/trace v1.38.0 // indirect
	go.yaml.in/yaml/v2 v2.4.2 // indirect
	golang.org/x/net v0.47.0 // indirect
	golang.org/x/sys v0.38.0 // indirect
	golang.org/x/text v0.31.0 // indirect
	google.golang.org/appengine v1.6.8 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8=
github.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
github.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs=
github.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f h1:MBcrTbmCf7CZa9yAwcB7ArveQb9TPVy4zFnQGz/LiUU=
github.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f/go.mod h1:UawoqorwkpZ58qWiL+nVJM0Po7FrzAdCxYVh9GgTTaA=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=
github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg=
github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=


================================================
FILE: internal/testutils/mysql.go
================================================
package testutils

import (
	"fmt"
	"log"
	"os"
	"path"
	"runtime"
	"strings"

	"github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	"github.com/whywaita/myshoes/pkg/datastore"
)

const schemaDirRelativePathFormat = "%s/../../pkg/datastore/mysql/%s"

func execSchema(fpath string) {
	b, err := os.ReadFile(fpath)
	if err != nil {
		log.Fatalf("schema reading error: %v", err)
	}

	queries := strings.Split(string(b), ";")

	for _, query := range queries[:len(queries)-1] {
		_, err = testDB.Exec(query)
		if err != nil {
			log.Fatalf("exec schema error: %v, query: %s", err, query)
		}
	}
}

func createTablesIfNotExist() {
	_, pwd, _, _ := runtime.Caller(0)
	schemaPath := fmt.Sprintf(schemaDirRelativePathFormat, path.Dir(pwd), "schema.sql")
	execSchema(schemaPath)
}

func truncateTables() {
	rows, err := testDB.Query("SHOW TABLES")
	if err != nil {
		log.Fatalf("show tables error: %#v", err)
	}
	defer rows.Close()

	for rows.Next() {
		var tableName string
		err = rows.Scan(&tableName)
		if err != nil {
			log.Fatalf("show table error: %#v", err)
			continue
		}

		cmds := []string{
			"SET FOREIGN_KEY_CHECKS = 0",
			fmt.Sprintf("TRUNCATE %s", tableName),
			"SET FOREIGN_KEY_CHECKS = 1",
		}
		for _, cmd := range cmds {
			_, err := testDB.Exec(cmd)

			if err != nil {
				mysqlErr, ok := err.(*mysql.MySQLError)

				if ok {
					if mysqlErr.Number == 0xde2 {
						// is rejected
						continue
					}
				} else {
					log.Fatalf("truncate error: %#v", err)
					continue
				}
			}
		}
	}
}

// GetTestDatastore return pointer of datastore
func GetTestDatastore() (datastore.Datastore, func()) {
	if testDatastore == nil {
		panic("datastore is not initialized yet")
	}

	return testDatastore, func() { truncateTables() }
}

// GetTestDB return pointer of testDB
func GetTestDB() (*sqlx.DB, func()) {
	if testDB == nil {
		panic("testDB is not initialized yet")
	}

	return testDB, func() { truncateTables() }
}


================================================
FILE: internal/testutils/testutils.go
================================================
package testutils

import (
	"fmt"
	"log"
	"net/http/httptest"
	"testing"

	"github.com/whywaita/myshoes/pkg/datastore"
	"github.com/whywaita/myshoes/pkg/datastore/mysql"
	"github.com/whywaita/myshoes/pkg/web"

	"github.com/jmoiron/sqlx"
	"github.com/ory/dockertest/v3"
)

const (
	mysqlRootPassword = "secret"
)

var (
	testDB        *sqlx.DB
	testDatastore datastore.Datastore

	testURL string
)

// IntegrationTestRunner is all integration test
func IntegrationTestRunner(m *testing.M) int {
	// uses a sensible default on windows (tcp/http) and linux/osx (socket)
	pool, err := dockertest.NewPool("")
	if err != nil {
		log.Fatalf("Could not connect to docker: %s", err)
	}

	// pulls an image, creates a container based on it and runs it
	resource, err := pool.Run("mysql", "8.0", []string{"MYSQL_ROOT_PASSWORD=" + mysqlRootPassword})
	if err != nil {
		log.Fatalf("Could not start resource: %s", err)
	}

	// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
	if err := pool.Retry(func() error {
		var err error
		dsn := fmt.Sprintf("root:%s@(localhost:%s)/mysql", mysqlRootPassword, resource.GetPort("3306/tcp"))
		testDatastore, err = mysql.New(dsn, make(chan<- struct{}))
		if err != nil {
			log.Fatalf("failed to create datastore instance: %s", err)
		}

		testDB, err = sqlx.Open("mysql", fmt.Sprintf("root:%s@(localhost:%s)/mysql?parseTime=true&loc=UTC", mysqlRootPassword, resource.GetPort("3306/tcp")))
		if err != nil {
			return err
		}
		return testDB.Ping()
	}); err != nil {
		log.Fatalf("Could not connect to docker: %s", err)
	}

	createTablesIfNotExist()
	//SetupDefaultFixtures()

	mux := web.NewMux(testDatastore)
	ts := httptest.NewServer(mux)
	testURL = ts.URL

	code := m.Run()

	ts.Close()
	truncateTables()

	// You can't defer this because os.Exit doesn't care for defer
	if err := pool.Purge(resource); err != nil {
		log.Fatalf("Could not purge resource: %s", err)
	}

	return code
}


================================================
FILE: internal/testutils/web.go
================================================
package testutils

// GetTestURL return url of httptest.Server
func GetTestURL() string {
	if testURL == "" {
		panic("testURL is not initialized yet")
	}

	return testURL
}


================================================
FILE: internal/util/util.go
================================================
package util

import (
	"math/rand/v2"
	"time"
)

// CalcRetryTime is caliculate retry time by exponential backoff and jitter
func CalcRetryTime(count int) time.Duration {
	if count == 0 {
		return 0
	}

	backoff := 1 << count
	jitter := time.Duration(rand.IntN(1000)) * time.Millisecond

	return time.Duration(backoff)*time.Second + jitter
}


================================================
FILE: pkg/config/config.go
================================================
package config

import (
	"crypto/rsa"
	"strings"
)

// Config is config value
var Config Conf

// Conf is type of Config
type Conf struct {
	GitHub GitHubApp

	MySQLDSN              string
	Port                  int
	ShoesPluginPath       string
	ShoesPluginOutputPath string
	RunnerUser            string
	RunnerBaseDirectory   string

	Debug           bool
	Strict          bool // check to registered runner before delete job
	ModeWebhookType ModeWebhookType

	MaxConnectionsToBackend int64
	MaxConcurrencyDeleting  int64

	GitHubURL     string
	RunnerVersion string

	DockerHubCredential     DockerHubCredential
	ProvideDockerHubMetrics bool
}

// DockerHubCredential is type of config value
type DockerHubCredential struct {
	Username string
	Password string
}

// GitHubApp is type of config value
type GitHubApp struct {
	AppID     int64
	AppSecret []byte
	PEMByte   []byte
	PEM       *rsa.PrivateKey
}

// Config Environment keys
const (
	EnvGitHubAppID               = "GITHUB_APP_ID"
	EnvGitHubAppSecret           = "GITHUB_APP_SECRET"
	EnvGitHubAppPrivateKeyBase64 = "GITHUB_PRIVATE_KEY_BASE64"
	EnvMySQLHost                 = "MYSQL_HOST"
	EnvMySQLPort                 = "MYSQL_PORT"
	EnvMySQLUser                 = "MYSQL_USER"
	EnvMySQLPassword             = "MYSQL_PASSWORD"
	EnvMySQLDatabase             = "MYSQL_DATABASE"
	EnvMySQLURL                  = "MYSQL_URL"
	EnvPort                      = "PORT"
	EnvShoesPluginPath           = "PLUGIN"
	EnvShoesPluginOutputPath     = "PLUGIN_OUTPUT"
	EnvRunnerUser                = "RUNNER_USER"
	EnvRunnerBaseDirectory       = "RUNNER_BASE_DIRECTORY"
	EnvDebug                     = "DEBUG"
	EnvStrict                    = "STRICT"
	EnvModeWebhookType           = "MODE_WEBHOOK_TYPE"
	EnvMaxConnectionsToBackend   = "MAX_CONNECTIONS_TO_BACKEND"
	EnvMaxConcurrencyDeleting    = "MAX_CONCURRENCY_DELETING"
	EnvGitHubURL                 = "GITHUB_URL"
	EnvRunnerVersion             = "RUNNER_VERSION"
	EnvDockerHubUsername         = "DOCKER_HUB_USERNAME"
	EnvDockerHubPassword         = "DOCKER_HUB_PASSWORD"
	EnvProvideDockerHubMetrics   = "PROVIDE_DOCKER_HUB_METRICS"
)

// ModeWebhookType is type value for GitHub webhook
type ModeWebhookType int

const (
	// ModeWebhookTypeUnknown is unknown
	ModeWebhookTypeUnknown ModeWebhookType = iota
	// ModeWebhookTypeCheckRun is check_run
	ModeWebhookTypeCheckRun
	// ModeWebhookTypeWorkflowJob is workflow_job
	ModeWebhookTypeWorkflowJob
)

// String is implementation of fmt.Stringer
func (mwt ModeWebhookType) String() string {
	unknown := "unknown"
	switch mwt {
	case ModeWebhookTypeUnknown:
		return unknown
	case ModeWebhookTypeCheckRun:
		return "check_run"
	case ModeWebhookTypeWorkflowJob:
		return "workflow_job"
	}

	return unknown
}

// Equal check in and value
func (mwt ModeWebhookType) Equal(in string) bool {
	return strings.EqualFold(in, mwt.String())
}

func marshalModeWebhookType(in string) ModeWebhookType {
	switch in {
	case "check_run":
		return ModeWebhookTypeCheckRun
	case "workflow_job":
		return ModeWebhookTypeWorkflowJob
	}

	return ModeWebhookTypeUnknown
}

// IsGHES return myshoes for GitHub Enterprise Server
func (c Conf) IsGHES() bool {
	return !strings.EqualFold(c.GitHubURL, "https://github.com")
}


================================================
FILE: pkg/config/init.go
================================================
package config

import (
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/hashicorp/go-version"
)

// Load load config from environment
func Load() {
	c := LoadWithDefault()

	ga := LoadGitHubApps()
	c.GitHub = *ga

	pluginPath := LoadPluginPath()
	c.ShoesPluginPath = pluginPath

	Config = c
}

// LoadWithDefault load only value that has default value
func LoadWithDefault() Conf {
	var c Conf

	p := "8080"
	if os.Getenv(EnvPort) != "" {
		p = os.Getenv(EnvPort)
	}
	pp, err := strconv.Atoi(p)
	if err != nil {
		log.Panicf("failed to parse PORT: %+v", err)
	}
	c.Port = pp

	runnerUser := "runner"
	if os.Getenv(EnvRunnerUser) != "" {
		runnerUser = os.Getenv(EnvRunnerUser)
	}
	c.RunnerUser = runnerUser

	c.RunnerBaseDirectory = "/tmp"
	if os.Getenv(EnvRunnerBaseDirectory) != "" {
		c.RunnerBaseDirectory = os.Getenv(EnvRunnerBaseDirectory)
		log.Printf("use runner base directory is %s\n", c.RunnerBaseDirectory)
	}

	c.Debug = false
	if os.Getenv(EnvDebug) == "true" {
		c.Debug = true
	}

	c.Strict = true
	if os.Getenv(EnvStrict) == "false" {
		c.Strict = false
	}

	c.ModeWebhookType = ModeWebhookTypeWorkflowJob
	if os.Getenv(EnvModeWebhookType) != "" {
		mwt := marshalModeWebhookType(os.Getenv(EnvModeWebhookType))

		if mwt == ModeWebhookTypeUnknown {
			log.Panicf("%s is invalid webhook type", os.Getenv(EnvModeWebhookType))
		}

		if mwt == ModeWebhookTypeCheckRun {
			log.Println("WARNING: check_run is deprecated mode and will delete it. Please use workflow_job")
		}

		c.ModeWebhookType = mwt
	}

	c.ProvideDockerHubMetrics = false
	if os.Getenv(EnvProvideDockerHubMetrics) == "true" {
		c.ProvideDockerHubMetrics = true
	}

	c.DockerHubCredential = DockerHubCredential{}
	if c.ProvideDockerHubMetrics {
		if os.Getenv(EnvDockerHubUsername) != "" && os.Getenv(EnvDockerHubPassword) != "" {
			c.DockerHubCredential.Username = os.Getenv(EnvDockerHubUsername)
			c.DockerHubCredential.Password = os.Getenv(EnvDockerHubPassword)
		} else {
			log.Println("WARNING: Providing Docker Hub metrics is enabled, but DOCKER_HUB_USERNAME and DOCKER_HUB_PASSWORD are not set. Providing Docker Hub metrics with anonymous user mode")
		}
	} else {
		log.Println("Docker Hub metrics is disabled")
	}

	c.MaxConnectionsToBackend = 50
	if os.Getenv(EnvMaxConnectionsToBackend) != "" {
		numberPB, err := strconv.ParseInt(os.Getenv(EnvMaxConnectionsToBackend), 10, 64)
		if err != nil {
			log.Panicf("failed to convert int64 %s: %+v", EnvMaxConnectionsToBackend, err)
		}
		c.MaxConnectionsToBackend = numberPB
	}
	c.MaxConcurrencyDeleting = 1
	if os.Getenv(EnvMaxConcurrencyDeleting) != "" {
		numberCD, err := strconv.ParseInt(os.Getenv(EnvMaxConcurrencyDeleting), 10, 64)
		if err != nil {
			log.Panicf("failed to convert int64 %s: %+v", EnvMaxConcurrencyDeleting, err)
		}
		c.MaxConcurrencyDeleting = numberCD
	}

	c.GitHubURL = "https://github.com"
	if os.Getenv(EnvGitHubURL) != "" {
		u, err := url.Parse(os.Getenv(EnvGitHubURL))
		if err != nil {
			log.Panicf("failed to parse URL %s: %+v", os.Getenv(EnvGitHubURL), err)
		}

		if strings.EqualFold(u.Scheme, "") {
			log.Panicf("%s must has scheme (value: %s)", EnvGitHubURL, os.Getenv(EnvGitHubURL))
		}
		if strings.EqualFold(u.Host, "") {
			log.Panicf("%s must has host (value: %s)", EnvGitHubURL, os.Getenv(EnvGitHubURL))
		}

		c.GitHubURL = os.Getenv(EnvGitHubURL)
	}

	if os.Getenv(EnvRunnerVersion) == "" {
		c.RunnerVersion = "latest"
	} else {
		// valid value: "latest" or "vX.XXX.X"
		switch os.Getenv(EnvRunnerVersion) {
		case "latest":
			c.RunnerVersion = "latest"
		default:
			_, err := version.NewVersion(os.Getenv(EnvRunnerVersion))
			if err != nil {
				log.Panicf("failed to parse input runner version: %+v", err)
			}

			c.RunnerVersion = os.Getenv(EnvRunnerVersion)
		}
	}

	c.ShoesPluginOutputPath = "."
	if os.Getenv(EnvShoesPluginOutputPath) != "" {
		c.ShoesPluginOutputPath = os.Getenv(EnvShoesPluginOutputPath)
	}

	Config = c
	return c
}

// LoadGitHubApps load config for GitHub Apps
func LoadGitHubApps() *GitHubApp {
	var ga GitHubApp
	appID, err := strconv.ParseInt(os.Getenv(EnvGitHubAppID), 10, 64)
	if err != nil {
		log.Panicf("failed to parse %s: %+v", EnvGitHubAppID, err)
	}
	ga.AppID = appID

	pemBase64ed := os.Getenv(EnvGitHubAppPrivateKeyBase64)
	if pemBase64ed == "" {
		log.Panicf("%s must be set", EnvGitHubAppPrivateKeyBase64)
	}
	pemByte, err := base64.StdEncoding.DecodeString(pemBase64ed)
	if err != nil {
		log.Panicf("failed to decode base64 %s: %+v", EnvGitHubAppPrivateKeyBase64, err)
	}
	ga.PEMByte = pemByte

	block, _ := pem.Decode(pemByte)
	if block == nil {
		log.Panicf("%s is invalid format, please input private key ", EnvGitHubAppPrivateKeyBase64)
	}
	key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
	if err != nil {
		log.Panicf("%s is invalid format, failed to parse private key: %+v", EnvGitHubAppPrivateKeyBase64, err)
	}
	ga.PEM = key

	appSecret := os.Getenv(EnvGitHubAppSecret)
	if appSecret == "" {
		log.Panicf("%s must be set", EnvGitHubAppSecret)
	}
	ga.AppSecret = []byte(appSecret)

	return &ga
}

// LoadMySQLURL load MySQL URL from environment
func LoadMySQLURL() string {
	mysqlHost, ok_Host := os.LookupEnv(EnvMySQLHost)
	mysqlPort, ok_Port := os.LookupEnv(EnvMySQLPort)
	mysqlUser, ok_User := os.LookupEnv(EnvMySQLUser)
	mysqlPassword, ok_Password := os.LookupEnv(EnvMySQLPassword)
	mysqlDatabase, ok_Database := os.LookupEnv(EnvMySQLDatabase)
	if ok_Host && ok_Port && ok_User && ok_Password && ok_Database {
		mysqlURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", mysqlUser, mysqlPassword, mysqlHost, mysqlPort, mysqlDatabase)
		log.Println("load MySQL URL from environment variables MYSQL_USER, MYSQL_PASSWORD, MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, not MYSQL_URL")
		return mysqlURL
	}
	mysqlURL := os.Getenv(EnvMySQLURL)
	if mysqlURL == "" {
		log.Panicf("%s must be set", EnvMySQLURL)
	}
	return mysqlURL
}

// LoadPluginPath load plugin path from environment
func LoadPluginPath() string {
	pluginPath := os.Getenv(EnvShoesPluginPath)
	if pluginPath == "" {
		log.Panicf("%s must be set", EnvShoesPluginPath)
	}
	fp, err := fetch(pluginPath)
	if err != nil {
		log.Panicf("failed to fetch plugin binary: %+v", err)
	}
	absPath, err := checkBinary(fp)
	if err != nil {
		log.Panicf("failed to check plugin binary: %+v", err)
	}
	log.Printf("use plugin path is %s\n", absPath)
	return absPath
}

func checkBinary(p string) (string, error) {
	f, err := os.ReadFile(p)
	if err != nil {
		return "", fmt.Errorf("failed to open file: %w", err)
	}

	// check binary type
	mineType := http.DetectContentType(f)
	if !strings.EqualFold(mineType, "application/octet-stream") {
		return "", fmt.Errorf("invalid file type (correct: application/octet-stream got: %s)", mineType)
	}

	// need permission of execute
	if err := os.Chmod(p, 0777); err != nil {
		return "", fmt.Errorf("failed to chmod: %w", err)
	}

	if filepath.IsAbs(p) {
		return p, nil
	}

	apath, err := filepath.Abs(p)
	if err != nil {
		return "", fmt.Errorf("failed to get abs: %w", err)
	}

	return apath, nil
}

// fetch retrieve plugin binaries.
// return saved file path.
func fetch(p string) (string, error) {
	_, err := os.Stat(p)
	if err == nil {
		// this is file path!
		return p, nil
	}

	u, err := url.Parse(p)
	if err != nil {
		return "", fmt.Errorf("failed to parse input url: %w", err)
	}
	switch u.Scheme {
	case "http", "https":
		return fetchHTTP(u)
	default:
		return "", fmt.Errorf("unsupported fetch schema (scheme: %s)", u.Scheme)
	}
}

// fetchHTTP fetch plugin binary over HTTP(s).
// save to current directory.
func fetchHTTP(u *url.URL) (string, error) {
	log.Printf("fetch plugin binary from %s\n", u.String())
	dir := Config.ShoesPluginOutputPath
	if strings.EqualFold(dir, ".") {
		pwd, err := os.Getwd()
		if err != nil {
			return "", fmt.Errorf("failed to working directory: %w", err)
		}
		dir = pwd
	}

	p := strings.Split(u.Path, "/")
	fileName := p[len(p)-1]

	fp := filepath.Join(dir, fileName)
	f, err := os.Create(fp)
	if err != nil {
		return "", fmt.Errorf("failed to create os file: %w", err)
	}
	defer f.Close()

	resp, err := http.Get(u.String())
	if err != nil {
		return "", fmt.Errorf("failed to get config via HTTP(S): %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get config via HTTP(S): status code is not 200 (status code: %d)", resp.StatusCode)
	}

	if _, err := io.Copy(f, resp.Body); err != nil {
		return "", fmt.Errorf("failed to write file (path: %s): %w", fp, err)
	}

	return fp, nil
}


================================================
FILE: pkg/datastore/github.go
================================================
package datastore

import (
	"context"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/whywaita/myshoes/pkg/logger"

	"github.com/google/go-github/v80/github"
	"github.com/whywaita/myshoes/pkg/gh"
)

// NewClientInstallationByRepo create a client of GitHub using installation ID from repo name
func NewClientInstallationByRepo(ctx context.Context, ds Datastore, repo string) (*github.Client, *Target, error) {
	target, err := SearchRepo(ctx, ds, repo)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to search repository: %w", err)
	}

	installationID, err := gh.IsInstalledGitHubApp(ctx, target.Scope)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get installation ID: %w", err)
	}

	client, err := gh.NewClientInstallation(installationID)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create client: %w", err)
	}

	return client, target, nil
}

// PendingWorkflowRunWithTarget is struct for pending workflow run
type PendingWorkflowRunWithTarget struct {
	Target      *Target
	WorkflowRun *github.WorkflowRun
}

// GetPendingWorkflowRunByRecentRepositories get pending workflow runs by recent active repositories
func GetPendingWorkflowRunByRecentRepositories(ctx context.Context, ds Datastore) ([]PendingWorkflowRunWithTarget, error) {
	pendingRuns, err := getPendingWorkflowRunByRecentRepositories(ctx, ds)
	if err != nil {
		return nil, fmt.Errorf("failed to get pending workflow runs: %w", err)
	}

	queuedJob, err := ds.ListJobs(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get list of jobs: %w", err)
	}

	var result []PendingWorkflowRunWithTarget
	// We ignore the pending run if the job is already queued.
	for _, pendingRun := range pendingRuns {
		found := false
		for _, job := range queuedJob {
			webhookEvent, err := github.ParseWebHook("workflow_job", []byte(job.CheckEventJSON))
			if err != nil {
				logger.Logf(false, "failed to parse webhook payload (job id: %s): %+v", job.UUID, err)
				continue
			}

			workflowJob, ok := webhookEvent.(*github.WorkflowJobEvent)
			if !ok {
				logger.Logf(false, "failed to cast to WorkflowJobEvent (job id: %s)", job.UUID)
				continue
			}

			if pendingRun.WorkflowRun.GetID() == workflowJob.GetWorkflowJob().GetRunID() {
				logger.Logf(true, "found job in datastore, So will ignore: (repo: %s, gh_run_id: %d, gh_job_id: %d)", pendingRun.WorkflowRun.GetRepository().GetFullName(), pendingRun.WorkflowRun.GetID(), workflowJob.GetWorkflowJob().GetID())
				found = true
				break
			}
		}

		if !found {
			result = append(result, pendingRun)
		}
	}

	return result, nil
}

func getPendingWorkflowRunByRecentRepositories(ctx context.Context, ds Datastore) ([]PendingWorkflowRunWithTarget, error) {
	recentActiveRepositories, err := getRecentRepositories(ctx, ds)
	if err != nil {
		return nil, fmt.Errorf("failed to get recent repositories: %w", err)
	}

	var pendingRuns []PendingWorkflowRunWithTarget
	var wg sync.WaitGroup
	var mu sync.Mutex
	for _, repoRawURL := range recentActiveRepositories {
		wg.Add(1)
		go func(repoRawURL string) {
			defer wg.Done()
			u, err := url.Parse(repoRawURL)
			if err != nil {
				logger.Logf(false, "failed to get pending run by recent repositories: failed to parse repository url: %+v", err)
				return
			}
			fullName := strings.TrimPrefix(u.Path, "/")
			client, target, err := NewClientInstallationByRepo(ctx, ds, fullName)
			if err != nil {
				logger.Logf(false, "failed to get pending run by recent repositories: failed to create a client of GitHub by repo (full_name: %s) %+v", fullName, err)
				return
			}

			owner, repo := gh.DivideScope(fullName)
			pendingRunsByRepo, err := getPendingRunByRepo(ctx, client, owner, repo)
			if err != nil {
				logger.Logf(false, "failed to get pending run by recent repositories: failed to get pending run by repo (full_name: %s) %+v", fullName, err)
				return
			}
			mu.Lock()
			for _, run := range pendingRunsByRepo {
				pendingRuns = append(pendingRuns, PendingWorkflowRunWithTarget{
					Target:      target,
					WorkflowRun: run,
				})
			}
			mu.Unlock()
		}(repoRawURL)
	}

	wg.Wait()

	return pendingRuns, nil
}

func getPendingRunByRepo(ctx context.Context, client *github.Client, owner, repo string) ([]*github.WorkflowRun, error) {
	runs, err := gh.ListWorkflowRunsNewest(ctx, client, owner, repo, 50)
	if err != nil {
		return nil, fmt.Errorf("failed to list runs: %w", err)
	}

	var pendingRuns []*github.WorkflowRun
	for _, r := range runs {
		if r.GetStatus() == "queued" || r.GetStatus() == "pending" {
			oldMinutes := 10
			sinceMinutes := time.Since(r.CreatedAt.Time).Minutes()
			if sinceMinutes >= float64(oldMinutes) {
				logger.Logf(false, "workflow run %d is pending over %d minutes, So will enqueue (repo: %s/%s)", r.GetID(), oldMinutes, owner, repo)
				pendingRuns = append(pendingRuns, r)
			} else {
				logger.Logf(true, "workflow run %d is pending, but not over %d minutes. So ignore (since: %f minutes, repo: %s/%s)", r.GetID(), oldMinutes, sinceMinutes, owner, repo)
			}
		}
	}

	return pendingRuns, nil
}

func getRecentRepositories(ctx context.Context, ds Datastore) ([]string, error) {
	recent := time.Now().Add(-1 * time.Hour)
	recentRunners, err := ds.ListRunnersLogBySince(ctx, recent)
	if err != nil {
		return nil, fmt.Errorf("failed to get targets from datastore: %w", err)
	}

	// sort by created_at
	sort.SliceStable(recentRunners, func(i, j int) bool {
		return recentRunners[i].CreatedAt.After(recentRunners[j].CreatedAt)
	})

	// unique repositories
	recentActiveRepositories := make(map[string]struct{})
	for _, r := range recentRunners {
		u := r.RepositoryURL
		if _, ok := recentActiveRepositories[u]; !ok {
			recentActiveRepositories[u] = struct{}{}
		}
	}
	var result []string
	for repository := range recentActiveRepositories {
		result = append(result, repository)
	}

	return result, nil
}


================================================
FILE: pkg/datastore/interface.go
================================================
package datastore

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	uuid "github.com/satori/go.uuid"

	"github.com/whywaita/myshoes/pkg/gh"
	"github.com/whywaita/myshoes/pkg/logger"
)

// Error values
var (
	ErrNotFound = errors.New("not found")
)

// Lock values
var (
	IsLocked    = "is locked"
	IsNotLocked = "is not locked"
)

// Datastore is persistent storage
type Datastore interface {
	CreateTarget(ctx context.Context, target Target) error
	GetTarget(ctx context.Context, id uuid.UUID) (*Target, error)
	GetTargetByScope(ctx context.Context, scope string) (*Target, error)
	ListTargets(ctx context.Context) ([]Target, error)
	DeleteTarget(ctx context.Context, id uuid.UUID) error

	// Deprecated: Use datastore.UpdateTargetStatus.
	UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus TargetStatus, description string) error
	UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error

	UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType ResourceType, newProviderURL sql.NullString) error

	EnqueueJob(ctx context.Context, job Job) error
	ListJobs(ctx context.Context) ([]Job, error)
	DeleteJob(ctx context.Context, id uuid.UUID) error

	CreateRunner(ctx context.Context, runner Runner) error
	ListRunners(ctx context.Context) ([]Runner, error)
	ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]Runner, error)
	ListRunnersLogBySince(ctx context.Context, since time.Time) ([]Runner, error)
	GetRunner(ctx context.Context, id uuid.UUID) (*Runner, error)
	DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason RunnerStatus) error

	// Lock
	GetLock(ctx context.Context) error
	IsLocked(ctx context.Context) (string, error)
}

// Target is a target repository that will add auto-scaling runner.
type Target struct {
	UUID  uuid.UUID `db:"uuid" json:"id"`
	Scope string    `db:"scope" json:"scope"` // repo (:owner/:repo) or org (:organization)
	// deprecated
	GitHubToken    string         `db:"github_token" json:"github_token"`
	TokenExpiredAt time.Time      `db:"token_expired_at" json:"token_expired_at"`
	GHEDomain      sql.NullString `db:"ghe_domain" json:"ghe_domain"`

	ResourceType      ResourceType   `db:"resource_type" json:"resource_type"`
	ProviderURL       sql.NullString `db:"provider_url" json:"provider_url"`
	Status            TargetStatus   `db:"status" json:"status"`
	StatusDescription sql.NullString `db:"status_description" json:"status_description"`
	CreatedAt         time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt         time.Time      `db:"updated_at" json:"updated_at"`
}

// OwnerRepo return :owner and :repo
func (t *Target) OwnerRepo() (string, string) {
	return gh.DivideScope(t.Scope)
}

// CanReceiveJob check status in target
func (t *Target) CanReceiveJob() bool {
	switch t.Status {
	case TargetStatusSuspend, TargetStatusDeleted:
		return false
	}

	return true
}

// ListTargets get list of target that can receive job
func ListTargets(ctx context.Context, ds Datastore) ([]Target, error) {
	targets, err := ds.ListTargets(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get targets from datastore: %w", err)
	}

	var result []Target

	for _, t := range targets {
		if t.CanReceiveJob() {
			result = append(result, t)
		}
	}

	return result, nil
}

// UpdateTargetStatus update datastore
func UpdateTargetStatus(ctx context.Context, ds Datastore, targetID uuid.UUID, newStatus TargetStatus, description string) error {
	target, err := ds.GetTarget(ctx, targetID)
	if err != nil {
		return fmt.Errorf("failed to get target: %w", err)
	}

	if !target.CanReceiveJob() {
		// not change status
		return nil
	}

	if err := ds.UpdateTargetStatus(ctx, targetID, newStatus, description); err != nil {
		logger.Logf(false, "failed to update target status: %+v", err)
		return err
	}

	return nil
}

// SearchRepo search datastore.Target from datastore
// format of repo is "orgs/repos"
func SearchRepo(ctx context.Context, ds Datastore, repo string) (*Target, error) {
	sep := strings.Split(repo, "/")
	if len(sep) != 2 {
		return nil, fmt.Errorf("incorrect repo format ex: orgs/repo (input: %s)", repo)
	}

	// use repo scope if set repo
	repoTarget, err := ds.GetTargetByScope(ctx, repo)
	if err == nil && repoTarget.CanReceiveJob() {
		return repoTarget, nil
	} else if err != nil && !errors.Is(err, ErrNotFound) {
		return nil, fmt.Errorf("failed to get target from repo: %w", err)
	}

	// repo is not found, so search org target
	org := sep[0]
	orgTarget, err := ds.GetTargetByScope(ctx, org)
	if err != nil {
		return nil, fmt.Errorf("failed to get target from organization: %w", err)
	}

	if !orgTarget.CanReceiveJob() {
		return nil, fmt.Errorf("target is not active")
	}

	return orgTarget, nil
}

// TargetStatus is status for target
type TargetStatus string

// TargetStatus variables
const (
	TargetStatusActive  TargetStatus = "active" //lint:ignore SA9004 this is status
	TargetStatusRunning              = "running"
	TargetStatusSuspend              = "suspend"
	TargetStatusDeleted              = "deleted"
	TargetStatusErr                  = "error"
)

// Job is a runner job
type Job struct {
	UUID           uuid.UUID      `db:"uuid"`
	GHEDomain      sql.NullString `db:"ghe_domain"`
	Repository     string         `db:"repository"` // repo (:owner/:repo)
	CheckEventJSON string         `db:"check_event"`
	TargetID       uuid.UUID      `db:"target_id"`
	CreatedAt      time.Time      `db:"created_at" json:"created_at"`
	UpdatedAt      time.Time      `db:"updated_at" json:"updated_at"`
}

// RepoURL return repository URL that send webhook.
func (j *Job) RepoURL() string {
	serverURL := "https://github.com"
	if j.GHEDomain.Valid {
		serverURL = j.GHEDomain.String
	}

	s := strings.Split(serverURL, "://")

	var u url.URL
	u.Scheme = s[0]
	u.Host = s[1]
	u.Path = j.Repository

	return u.String()
}

// Runner is a runner
type Runner struct {
	UUID           uuid.UUID      `db:"runner_id"`
	ShoesType      string         `db:"shoes_type"`
	IPAddress      string         `db:"ip_address"`
	TargetID       uuid.UUID      `db:"target_id"`
	CloudID        string         `db:"cloud_id"`
	Deleted        bool           `db:"deleted"`
	Status         RunnerStatus   `db:"status"`
	ResourceType   ResourceType   `db:"resource_type"`
	RunnerUser     sql.NullString `db:"runner_user" json:"runner_user"`
	ProviderURL    sql.NullString `db:"provider_url" json:"provider_url"`
	RepositoryURL  string         `db:"repository_url"`
	RequestWebhook string         `db:"request_webhook"`
	CreatedAt      time.Time      `db:"created_at"`
	UpdatedAt      time.Time      `db:"updated_at"`
	DeletedAt      sql.NullTime   `db:"deleted_at"`
}

// RunnerStatus is status for runner
type RunnerStatus string

// RunnerStatus variables
const (
	RunnerStatusCreated        RunnerStatus = "created" //lint:ignore SA9004 this is status
	RunnerStatusCompleted                   = "completed"
	RunnerStatusReachHardLimit              = "reach_hard_limit"
)


================================================
FILE: pkg/datastore/memory/memory.go
================================================
package memory

import (
	"context"
	"database/sql"
	"fmt"
	"sync"
	"time"

	uuid "github.com/satori/go.uuid"

	"github.com/whywaita/myshoes/pkg/datastore"
)

// Memory is implement datastore on-memory
type Memory struct {
	mu      *sync.RWMutex
	targets map[uuid.UUID]datastore.Target
	jobs    map[uuid.UUID]datastore.Job
	runners map[uuid.UUID]datastore.Runner
}

// New create map
func New() (*Memory, error) {
	m := &sync.RWMutex{}
	t := map[uuid.UUID]datastore.Target{}
	j := map[uuid.UUID]datastore.Job{}
	r := map[uuid.UUID]datastore.Runner{}

	return &Memory{
		mu:      m,
		targets: t,
		jobs:    j,
		runners: r,
	}, nil
}

// CreateTarget create a target
func (m *Memory) CreateTarget(ctx context.Context, target datastore.Target) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.targets[target.UUID] = target
	return nil
}

// GetTarget get a target
func (m *Memory) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	t, ok := m.targets[id]
	if !ok {
		return nil, datastore.ErrNotFound
	}
	return &t, nil
}

// GetTargetByScope get a target from scope
func (m *Memory) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	for _, t := range m.targets {
		if t.Scope == scope {
			// found
			return &t, nil

		}
	}

	return nil, datastore.ErrNotFound
}

// ListTargets get a all targets
func (m *Memory) ListTargets(ctx context.Context) ([]datastore.Target, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var targets []datastore.Target

	for _, t := range m.targets {
		targets = append(targets, t)
	}

	return targets, nil
}

// DeleteTarget delete a target
func (m *Memory) DeleteTarget(ctx context.Context, id uuid.UUID) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	delete(m.targets, id)
	return nil
}

// UpdateTargetStatus update status in target
func (m *Memory) UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus datastore.TargetStatus, description string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	t, ok := m.targets[targetID]
	if !ok {
		return fmt.Errorf("not found")
	}

	t.Status = newStatus
	if description != "" {
		t.StatusDescription.Valid = true
	} else {
		t.StatusDescription.Valid = false
	}
	t.StatusDescription.String = description

	m.targets[targetID] = t

	return nil
}

// UpdateToken update token in target
func (m *Memory) UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	t, ok := m.targets[targetID]
	if !ok {
		return fmt.Errorf("not found")
	}
	t.GitHubToken = newToken
	t.TokenExpiredAt = newExpiredAt

	m.targets[targetID] = t
	return nil
}

// UpdateTargetParam update parameter of target
func (m *Memory) UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType datastore.ResourceType, newProviderURL string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	t, ok := m.targets[targetID]
	if !ok {
		return fmt.Errorf("not found")
	}
	t.ResourceType = newResourceType
	t.ProviderURL = sql.NullString{
		String: newProviderURL,
		Valid:  true,
	}

	m.targets[targetID] = t
	return nil
}

// EnqueueJob add a job
func (m *Memory) EnqueueJob(ctx context.Context, job datastore.Job) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.jobs[job.UUID] = job
	return nil
}

// ListJobs get all jobs
func (m *Memory) ListJobs(ctx context.Context) ([]datastore.Job, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	var jobs []datastore.Job
	for _, j := range m.jobs {
		jobs = append(jobs, j)
	}

	return jobs, nil
}

// DeleteJob delete a job
func (m *Memory) DeleteJob(ctx context.Context, id uuid.UUID) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	delete(m.jobs, id)
	return nil
}

// CreateRunner add a runner
func (m *Memory) CreateRunner(ctx context.Context, runner datastore.Runner) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.runners[runner.UUID] = runner

	return nil
}

// ListRunners get a all runners
func (m *Memory) ListRunners(ctx context.Context) ([]datastore.Runner, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	var runners []datastore.Runner
	for _, r := range m.runners {
		runners = append(runners, r)
	}

	return runners, nil
}

// ListRunnersByTargetID get a not deleted runners that has target_id
func (m *Memory) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	var runners []datastore.Runner
	for _, r := range m.runners {
		if uuid.Equal(r.TargetID, targetID) {
			runners = append(runners, r)
		}
	}

	return runners, nil
}

// ListRunnersLogBySince ListRunnerLog get a runners since time
func (m *Memory) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	var runners []datastore.Runner
	for _, r := range m.runners {
		if r.CreatedAt.After(since) {
			runners = append(runners, r)
		}
	}

	return runners, nil
}

// GetRunner get a runner
func (m *Memory) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	r, ok := m.runners[id]
	if !ok {
		return nil, datastore.ErrNotFound
	}

	return &r, nil
}

// DeleteRunner delete a runner
func (m *Memory) DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason datastore.RunnerStatus) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	delete(m.runners, id)
	return nil
}

// GetLock get lock
func (m *Memory) GetLock(ctx context.Context) error {
	return nil
}

// IsLocked return status of lock
func (m *Memory) IsLocked(ctx context.Context) (string, error) {
	return datastore.IsNotLocked, nil
}


================================================
FILE: pkg/datastore/mysql/job.go
================================================
package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"

	uuid "github.com/satori/go.uuid"
	"github.com/whywaita/myshoes/pkg/datastore"
)

// EnqueueJob add a job
func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error {
	query := `INSERT INTO jobs(uuid, ghe_domain, repository, check_event, target_id) VALUES (?, ?, ?, ?, ?)`
	if _, err := m.Conn.ExecContext(ctx, query, job.UUID, job.GHEDomain, job.Repository, job.CheckEventJSON, job.TargetID.String()); err != nil {
		return fmt.Errorf("failed to execute INSERT query: %w", err)
	}

	select {
	case m.notifyEnqueueCh <- struct{}{}:
		// notified to starter
	default:
		// no capacity on channel, do not block
	}

	return nil
}

// ListJobs get all jobs
func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) {
	var jobs []datastore.Job
	query := `SELECT uuid, ghe_domain, repository, check_event, target_id, created_at, updated_at FROM jobs`
	if err := m.Conn.SelectContext(ctx, &jobs, query); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return jobs, nil
}

// DeleteJob delete a job
func (m *MySQL) DeleteJob(ctx context.Context, id uuid.UUID) error {
	query := `DELETE FROM jobs WHERE uuid = ?`
	if _, err := m.Conn.ExecContext(ctx, query, id.String()); err != nil {
		return fmt.Errorf("failed to execute DELETE query: %w", err)
	}

	return nil
}


================================================
FILE: pkg/datastore/mysql/job_test.go
================================================
package mysql_test

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/jmoiron/sqlx"
	uuid "github.com/satori/go.uuid"

	"github.com/whywaita/myshoes/internal/testutils"
	"github.com/whywaita/myshoes/pkg/datastore"
)

var testJobID = uuid.FromStringOrNil("1b4e5b7a-e3c1-4829-9cfd-eac4183f2c95")

func TestMySQL_EnqueueJob(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:  testTargetID,
		Scope: testScopeRepo,
		GHEDomain: sql.NullString{
			Valid: false,
		},
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	tests := []struct {
		input datastore.Job
		want  *datastore.Job
		err   bool
	}{
		{
			input: datastore.Job{
				UUID:           testJobID,
				Repository:     testScopeRepo,
				CheckEventJSON: `{"example": "json"}`,
				TargetID:       testTargetID,
			},
			want: &datastore.Job{
				UUID:           testJobID,
				Repository:     testScopeRepo,
				CheckEventJSON: `{"example": "json"}`,
				TargetID:       testTargetID,
			},
			err: false,
		},
	}

	for _, test := range tests {
		err := testDatastore.EnqueueJob(context.Background(), test.input)
		if !test.err && err != nil {
			t.Fatalf("failed to enqueue job: %+v", err)
		}
		got, err := getJobFromSQL(testDB, test.input.UUID)
		if err != nil {
			t.Fatalf("failed to get job from SQL: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}
		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_ListJobs(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:  testTargetID,
		Scope: testScopeRepo,
		GHEDomain: sql.NullString{
			Valid: false,
		},
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	tests := []struct {
		input []datastore.Job
		want  []datastore.Job
		err   bool
	}{
		{
			input: []datastore.Job{
				{
					UUID:           testJobID,
					Repository:     testScopeRepo,
					CheckEventJSON: `{"example": "json"}`,
					TargetID:       testTargetID,
				},
			},
			want: []datastore.Job{
				{
					UUID:           testJobID,
					Repository:     testScopeRepo,
					CheckEventJSON: `{"example": "json"}`,
					TargetID:       testTargetID,
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		for _, input := range test.input {
			err := testDatastore.EnqueueJob(context.Background(), input)
			if !test.err && err != nil {
				t.Fatalf("failed to enqueue job: %+v", err)
			}
		}

		got, err := testDatastore.ListJobs(context.Background())
		if err != nil {
			t.Fatalf("failed to get jobs: %+v", err)
		}
		if len(test.want) != len(got) {
			t.Fatalf("incorrect length jobs, want: %d but got: %d", len(test.want), len(got))
		}
		for i := range got {
			got[i].CreatedAt = time.Time{}
			got[i].UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_DeleteJob(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:  testTargetID,
		Scope: testScopeRepo,
		GHEDomain: sql.NullString{
			Valid: false,
		},
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	if err := testDatastore.EnqueueJob(context.Background(), datastore.Job{
		UUID:           testJobID,
		Repository:     testScopeRepo,
		CheckEventJSON: `{"example": "json"}`,
		TargetID:       testTargetID,
	}); err != nil {
		t.Fatalf("failed to enqueue job: %+v", err)
	}

	tests := []struct {
		input uuid.UUID
		want  *datastore.Job
		err   bool
	}{
		{
			input: testJobID,
			want:  nil,
			err:   false,
		},
	}

	for _, test := range tests {
		err := testDatastore.DeleteJob(context.Background(), test.input)
		if !test.err && err != nil {
			t.Fatalf("failed to delete job: %+v", err)
		}

		got, err := getJobFromSQL(testDB, test.input)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			t.Fatalf("failed to get job from SQL: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func getJobFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Job, error) {
	var j datastore.Job
	query := `SELECT uuid, ghe_domain, repository, check_event, target_id FROM jobs WHERE uuid = ?`
	stmt, err := testDB.Preparex(query)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare: %w", err)
	}
	err = stmt.Get(&j, id)
	if err != nil {
		return nil, fmt.Errorf("failed to get job: %w", err)
	}
	return &j, nil
}


================================================
FILE: pkg/datastore/mysql/lock.go
================================================
package mysql

import (
	"context"
	"fmt"

	"github.com/go-sql-driver/mysql"
	"github.com/whywaita/myshoes/pkg/config"
	"github.com/whywaita/myshoes/pkg/datastore"
)

// GetLock get lock
func (m *MySQL) GetLock(ctx context.Context) error {
	var res int

	cfg, err := mysql.ParseDSN(config.Config.MySQLDSN)
	if err != nil {
		return fmt.Errorf("failed to parse DSN: %w", err)
	}
	lockKey := cfg.DBName

	query := fmt.Sprintf(`SELECT GET_LOCK('%s', 10)`, lockKey)
	if err := m.Conn.GetContext(ctx, &res, query); err != nil {
		return fmt.Errorf("failed to GET_LOCK: %w", err)
	}

	return nil
}

// IsLocked return status of lock
func (m *MySQL) IsLocked(ctx context.Context) (string, error) {
	var res int

	cfg, err := mysql.ParseDSN(config.Config.MySQLDSN)
	if err != nil {
		return "", fmt.Errorf("failed to parse DSN: %w", err)
	}
	lockKey := cfg.DBName

	query := fmt.Sprintf(`SELECT IS_FREE_LOCK('%s')`, lockKey)
	if err := m.Conn.GetContext(ctx, &res, query); err != nil {
		return "", fmt.Errorf("failed to IS_FREE_LOCK: %w", err)
	}

	switch res {
	case 1:
		return datastore.IsNotLocked, nil
	case 0:
		return datastore.IsLocked, nil
	}

	return "", fmt.Errorf("IS_FREE_LOCK return NULL")
}


================================================
FILE: pkg/datastore/mysql/mysql.go
================================================
package mysql

import (
	"fmt"
	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
)

// MySQL is implement datastore in MySQL
type MySQL struct {
	Conn *sqlx.DB

	notifyEnqueueCh chan<- struct{}
}

// New create mysql connection
func New(dsn string, notifyEnqueueCh chan<- struct{}) (*MySQL, error) {
	u, err := getMySQLURL(dsn)
	if err != nil {
		return nil, fmt.Errorf("failed to get MySQL URL: %w", err)
	}

	conn, err := sqlx.Open("mysql", u)
	if err != nil {
		return nil, fmt.Errorf("failed to create mysql connection: %w", err)
	}

	return &MySQL{
		Conn:            conn,
		notifyEnqueueCh: notifyEnqueueCh,
	}, nil
}

func getMySQLURL(dsn string) (string, error) {
	c, err := mysql.ParseDSN(dsn)
	if err != nil {
		return "", fmt.Errorf("failed to parse DSN: %w", err)
	}

	c.Loc = time.UTC
	c.ParseTime = true
	c.Collation = "utf8mb4_general_ci"
	if c.Params == nil {
		c.Params = map[string]string{}
	}
	c.Params["sql_mode"] = "'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'"

	c.InterpolateParams = true

	return c.FormatDSN(), nil
}


================================================
FILE: pkg/datastore/mysql/mysql_test.go
================================================
package mysql_test

import (
	"os"
	"testing"

	"github.com/whywaita/myshoes/internal/testutils"
)

func TestMain(m *testing.M) {
	os.Exit(testutils.IntegrationTestRunner(m))
}


================================================
FILE: pkg/datastore/mysql/runner.go
================================================
package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	uuid "github.com/satori/go.uuid"
	"github.com/whywaita/myshoes/pkg/datastore"
)

// CreateRunner add a runner
func (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runner) error {
	tx := m.Conn.MustBegin()

	queryRunner := `INSERT INTO runners(uuid) VALUES (?)`
	if _, err := tx.ExecContext(ctx, queryRunner, runner.UUID.String()); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute INSERT query runners: %w", err)
	}

	queryDetail := `INSERT INTO runner_detail(runner_id, shoes_type, ip_address, target_id, cloud_id, resource_type, runner_user, repository_url, request_webhook, provider_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
	if _, err := tx.ExecContext(ctx, queryDetail, runner.UUID.String(), runner.ShoesType, runner.IPAddress, runner.TargetID.String(), runner.CloudID, runner.ResourceType, runner.RunnerUser, runner.RepositoryURL, runner.RequestWebhook, runner.ProviderURL); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute INSERT query runner_detail: %w", err)
	}

	queryRunning := `INSERT INTO runners_running(runner_id) VALUES (?)`
	if _, err := tx.ExecContext(ctx, queryRunning, runner.UUID.String()); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute INSERT query runners_running: %w", err)
	}

	if err := tx.Commit(); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute COMMIT: %w", err)
	}
	return nil
}

// ListRunners get a not deleted runners
func (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, error) {
	var runners []datastore.Runner
	query := `SELECT runner.runner_id, detail.shoes_type, detail.ip_address, detail.target_id, detail.cloud_id, detail.created_at, detail.updated_at, detail.resource_type, detail.repository_url, detail.request_webhook, detail.runner_user, detail.provider_url
 FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id`
	err := m.Conn.SelectContext(ctx, &runners, query)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return runners, nil
}

// ListRunnersByTargetID get a not deleted runners that has target_id
func (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) {
	var runners []datastore.Runner
	query := `SELECT runner.runner_id, detail.shoes_type, detail.ip_address, detail.target_id, detail.cloud_id, detail.created_at, detail.updated_at, detail.resource_type, detail.repository_url, detail.request_webhook, detail.runner_user, detail.provider_url
 FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id WHERE detail.target_id = ?`
	err := m.Conn.SelectContext(ctx, &runners, query, targetID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return runners, nil
}

// ListRunnersLogBySince ListRunnerLog get a runners since time
func (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) {
	var runners []datastore.Runner

	query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE created_at > ?`
	err := m.Conn.SelectContext(ctx, &runners, query, since)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return runners, nil
}

// GetRunner get a runner
func (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) {
	var r datastore.Runner

	query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE runner_id = ?`
	if err := m.Conn.GetContext(ctx, &r, query, id.String()); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return &r, nil
}

// DeleteRunner delete a runner
func (m *MySQL) DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason datastore.RunnerStatus) error {
	tx := m.Conn.MustBegin()

	queryDelete := `DELETE FROM runners_running WHERE runner_id = ?`
	if _, err := tx.ExecContext(ctx, queryDelete, id.String()); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute DELETE query: %w", err)
	}

	queryInsert := `INSERT INTO runners_deleted(runner_id, reason) VALUES (?, ?)`
	if _, err := tx.ExecContext(ctx, queryInsert, id.String(), reason); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute INSERT query: %w", err)
	}

	if err := tx.Commit(); err != nil {
		tx.Rollback()
		return fmt.Errorf("failed to execute COMMIT: %w", err)
	}

	return nil
}


================================================
FILE: pkg/datastore/mysql/runner_test.go
================================================
package mysql_test

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/jmoiron/sqlx"

	uuid "github.com/satori/go.uuid"
	"github.com/whywaita/myshoes/internal/testutils"
	"github.com/whywaita/myshoes/pkg/datastore"
)

var testRunnerID = uuid.FromStringOrNil("7943e412-c0ae-4068-ab24-3e71a13fbe53")

func TestMySQL_CreateRunner(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	tests := []struct {
		input datastore.Runner
		want  *datastore.Runner
		err   bool
	}{
		{
			input: datastore.Runner{
				UUID:           testRunnerID,
				ShoesType:      "shoes-test",
				TargetID:       testTargetID,
				CloudID:        "mycloud-uuid",
				ResourceType:   datastore.ResourceTypeNano,
				RepositoryURL:  "https://github.com/octocat/Hello-World",
				RequestWebhook: "{}",
			},
			want: &datastore.Runner{
				UUID:           testRunnerID,
				ShoesType:      "shoes-test",
				TargetID:       testTargetID,
				CloudID:        "mycloud-uuid",
				ResourceType:   datastore.ResourceTypeNano,
				RepositoryURL:  "https://github.com/octocat/Hello-World",
				RequestWebhook: "{}",
			},
			err: false,
		},
		{
			input: datastore.Runner{
				UUID:         testRunnerID,
				ShoesType:    "shoes-test",
				TargetID:     testTargetID,
				CloudID:      "mycloud-uuid",
				ResourceType: datastore.ResourceTypeNano,
				RunnerUser: sql.NullString{
					String: "runner",
					Valid:  true,
				},
				ProviderURL: sql.NullString{
					String: "./shoes-test",
					Valid:  true,
				},
				RepositoryURL:  "https://github.com/octocat/Hello-World",
				RequestWebhook: "{}",
			},
			want: &datastore.Runner{
				UUID:         testRunnerID,
				ShoesType:    "shoes-test",
				TargetID:     testTargetID,
				CloudID:      "mycloud-uuid",
				ResourceType: datastore.ResourceTypeNano,
				RunnerUser: sql.NullString{
					String: "runner",
					Valid:  true,
				},
				ProviderURL: sql.NullString{
					String: "./shoes-test",
					Valid:  true,
				},
				RepositoryURL:  "https://github.com/octocat/Hello-World",
				RequestWebhook: "{}",
			},
			err: false,
		},
	}

	for _, test := range tests {
		if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
			UUID:           testTargetID,
			Scope:          testScopeRepo,
			GitHubToken:    testGitHubToken,
			TokenExpiredAt: testTime,
			ResourceType:   datastore.ResourceTypeNano,
		}); err != nil {
			t.Fatalf("failed to create target: %+v", err)
		}

		err := testDatastore.CreateRunner(context.Background(), test.input)
		if !test.err && err != nil {
			t.Fatalf("failed to create runner: %+v", err)
		}
		got, err := getRunnerFromSQL(testDB, test.input.UUID)
		if err != nil {
			t.Fatalf("failed to get runner from SQL: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}

		teardown()
	}
}

func TestMySQL_ListRunners(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	tests := []struct {
		input []datastore.Runner
		want  []datastore.Runner
		err   bool
	}{
		{
			input: []datastore.Runner{
				{
					UUID:           testRunnerID,
					ShoesType:      "shoes-test",
					TargetID:       testTargetID,
					CloudID:        "mycloud-uuid",
					ResourceType:   datastore.ResourceTypeNano,
					RepositoryURL:  "https://github.com/octocat/Hello-World",
					RequestWebhook: "{}",
				},
			},
			want: []datastore.Runner{
				{
					UUID:           testRunnerID,
					ShoesType:      "shoes-test",
					TargetID:       testTargetID,
					CloudID:        "mycloud-uuid",
					ResourceType:   datastore.ResourceTypeNano,
					RepositoryURL:  "https://github.com/octocat/Hello-World",
					RequestWebhook: "{}",
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		for _, input := range test.input {
			err := testDatastore.CreateRunner(context.Background(), input)
			if !test.err && err != nil {
				t.Fatalf("failed to create runner: %+v", err)
			}
		}

		got, err := testDatastore.ListRunners(context.Background())
		if err != nil {
			t.Fatalf("failed to get runners: %+v", err)
		}
		if len(test.want) != len(got) {
			t.Fatalf("incorrect length runners, want: %d but got: %d", len(test.want), len(got))
		}
		for i := range got {
			got[i].CreatedAt = time.Time{}
			got[i].UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	u := "00000000-0000-0000-0000-00000000000%d"

	for i := 0; i < 3; i++ {
		input := datastore.Runner{
			UUID:           testRunnerID,
			ShoesType:      "shoes-test",
			TargetID:       testTargetID,
			CloudID:        "mycloud-uuid",
			ResourceType:   datastore.ResourceTypeNano,
			RepositoryURL:  "https://github.com/octocat/Hello-World",
			RequestWebhook: "{}",
		}
		input.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
		err := testDatastore.CreateRunner(context.Background(), input)
		if err != nil {
			t.Fatalf("failed to create runner: %+v", err)
		}
	}

	err := testDatastore.DeleteRunner(context.Background(), uuid.FromStringOrNil(fmt.Sprintf(u, 0)), time.Now(), "deleted")
	if err != nil {
		t.Fatalf("failed to delete runner: %+v", err)
	}

	got, err := testDatastore.ListRunners(context.Background())
	if err != nil {
		t.Fatalf("failed to get runners: %+v", err)
	}
	for i := range got {
		got[i].CreatedAt = time.Time{}
		got[i].UpdatedAt = time.Time{}
	}

	var want []datastore.Runner
	for i := 1; i < 3; i++ {
		r := datastore.Runner{
			UUID:           testRunnerID,
			ShoesType:      "shoes-test",
			TargetID:       testTargetID,
			CloudID:        "mycloud-uuid",
			ResourceType:   datastore.ResourceTypeNano,
			RepositoryURL:  "https://github.com/octocat/Hello-World",
			RequestWebhook: "{}",
		}
		r.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
		want = append(want, r)
	}

	if diff := cmp.Diff(want, got); diff != "" {
		t.Errorf("mismatch (-want +got):\n%s", diff)
	}
}

func TestMySQL_ListRunnersLogBySince(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	u := "00000000-0000-0000-0000-00000000000%d"

	for i := 1; i < 3; i++ {
		input := datastore.Runner{
			UUID:           testRunnerID,
			ShoesType:      "shoes-test",
			TargetID:       testTargetID,
			CloudID:        "mycloud-uuid",
			ResourceType:   datastore.ResourceTypeNano,
			RepositoryURL:  "https://github.com/octocat/Hello-World",
			RequestWebhook: "{}",
		}
		input.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
		err := testDatastore.CreateRunner(context.Background(), input)
		if err != nil {
			t.Fatalf("failed to create runner: %+v", err)
		}
		time.Sleep(500 * time.Millisecond)
	}

	recent := time.Now().Add(-10 * time.Second)
	got, err := testDatastore.ListRunnersLogBySince(context.Background(), recent)
	if err != nil {
		t.Fatalf("failed to get runners: %+v", err)
	}
	for i := range got {
		got[i].CreatedAt = time.Time{}
		got[i].UpdatedAt = time.Time{}
	}

	var want []datastore.Runner
	for i := 1; i < 3; i++ {
		r := datastore.Runner{
			UUID:           testRunnerID,
			ShoesType:      "shoes-test",
			TargetID:       testTargetID,
			CloudID:        "mycloud-uuid",
			ResourceType:   datastore.ResourceTypeNano,
			RepositoryURL:  "https://github.com/octocat/Hello-World",
			RequestWebhook: "{}",
		}
		r.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))
		want = append(want, r)
	}

	if diff := cmp.Diff(want, got); diff != "" {
		t.Errorf("mismatch (-want +got):\n%s", diff)
	}
}

func TestMySQL_GetRunner(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	if err := testDatastore.CreateRunner(context.Background(), datastore.Runner{
		UUID:           testRunnerID,
		ShoesType:      "shoes-test",
		TargetID:       testTargetID,
		CloudID:        "mycloud-uuid",
		ResourceType:   datastore.ResourceTypeNano,
		RepositoryURL:  "https://github.com/octocat/Hello-World",
		RequestWebhook: "{}",
	}); err != nil {
		t.Fatalf("failed to create runner: %+v", err)
	}

	tests := []struct {
		input uuid.UUID
		want  *datastore.Runner
		err   bool
	}{
		{
			input: testRunnerID,
			want: &datastore.Runner{
				UUID:           testRunnerID,
				ShoesType:      "shoes-test",
				TargetID:       testTargetID,
				CloudID:        "mycloud-uuid",
				ResourceType:   datastore.ResourceTypeNano,
				RepositoryURL:  "https://github.com/octocat/Hello-World",
				RequestWebhook: "{}",
			},
			err: false,
		},
	}

	for _, test := range tests {
		got, err := testDatastore.GetRunner(context.Background(), test.input)
		if err != nil {
			t.Fatalf("failed to get runner: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_DeleteRunner(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	if err := testDatastore.CreateRunner(context.Background(), datastore.Runner{
		UUID:           testRunnerID,
		ShoesType:      "shoes-test",
		TargetID:       testTargetID,
		CloudID:        "mycloud-uuid",
		ResourceType:   datastore.ResourceTypeNano,
		RepositoryURL:  "https://github.com/octocat/Hello-World",
		RequestWebhook: "{}",
	}); err != nil {
		t.Fatalf("failed to create runner: %+v", err)
	}

	deleted := datastore.Runner{
		UUID:           testRunnerID,
		ShoesType:      "shoes-test",
		TargetID:       testTargetID,
		CloudID:        "mycloud-uuid",
		ResourceType:   datastore.ResourceTypeNano,
		RepositoryURL:  "https://github.com/octocat/Hello-World",
		RequestWebhook: "{}",
	}

	tests := []struct {
		input uuid.UUID
		want  *datastore.Runner
		err   bool
	}{
		{
			input: testRunnerID,
			want:  &deleted,
			err:   false,
		},
	}

	for _, test := range tests {
		err := testDatastore.DeleteRunner(context.Background(), test.input, time.Now().UTC(), datastore.RunnerStatusCompleted)
		if !test.err && err != nil {
			t.Fatalf("failed to create target: %+v", err)
		}
		got, err := getRunnerFromSQL(testDB, test.input)
		if err != nil {
			t.Fatalf("failed to get target from SQL: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
			got.DeletedAt = sql.NullTime{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}

		if _, err := getRunningRunnerFromSQL(testDB, test.input); err == nil || errors.Is(err, sql.ErrNoRows) {
			t.Errorf("%s is deleted, but exist in runner_running: %+v", test.input, err)
		}
		if _, err := getDeletedRunnerFromSQL(testDB, test.input); err != nil {
			t.Fatalf("%s is not exist in runners_deleted: %+v", test.input, err)
		}
	}
}

func getRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {
	var r datastore.Runner
	query := `SELECT runner_id, shoes_type, ip_address, target_id, cloud_id, created_at, updated_at, resource_type, repository_url, request_webhook, runner_user, provider_url FROM runner_detail WHERE runner_id = ?`
	stmt, err := testDB.Preparex(query)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare: %w", err)
	}
	err = stmt.Get(&r, id)
	if err != nil {
		return nil, fmt.Errorf("failed to get runner: %w", err)
	}
	return &r, nil
}

func getRunningRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {
	var r datastore.Runner
	query := `SELECT detail.runner_id, shoes_type, ip_address, target_id, cloud_id, detail.created_at, updated_at, detail.resource_type, detail.repository_url, detail.request_webhook
FROM runner_detail AS detail JOIN runnesr_running AS running ON detail.runner_id = running.runner_id WHERE detail.runner_id = ?`
	stmt, err := testDB.Preparex(query)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare: %w", err)
	}
	err = stmt.Get(&r, id)
	if err != nil {
		return nil, fmt.Errorf("failed to get runner: %w", err)
	}
	return &r, nil
}

func getDeletedRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {
	var r datastore.Runner
	query := `SELECT detail.runner_id, shoes_type, ip_address, target_id, cloud_id, detail.created_at, updated_at, detail.resource_type, detail.repository_url, detail.request_webhook
FROM runner_detail AS detail JOIN runners_deleted AS deleted ON detail.runner_id = deleted.runner_id WHERE detail.runner_id = ?`
	stmt, err := testDB.Preparex(query)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare: %w", err)
	}
	err = stmt.Get(&r, id)
	if err != nil {
		return nil, fmt.Errorf("failed to get runner: %w", err)
	}
	return &r, nil
}


================================================
FILE: pkg/datastore/mysql/schema.sql
================================================
CREATE TABLE `targets` (
    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,
    `scope` VARCHAR(255) NOT NULL,
    `ghe_domain` VARCHAR(255),
    `github_token` VARCHAR(255) NOT NULL,
    `token_expired_at` TIMESTAMP NOT NULL,
    `resource_type` ENUM('nano', 'micro', 'small', 'medium', 'large', 'xlarge', '2xlarge', '3xlarge', '4xlarge') NOT NULL,
    `provider_url` VARCHAR(255),
    `status` VARCHAR(255) NOT NULL DEFAULT 'active',
    `status_description` VARCHAR(255),
    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
    `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,
    UNIQUE KEY `ghe_domain_scope` (`ghe_domain`, `scope`)
);

CREATE TABLE `runners` (
    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,
    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp
);

CREATE TABLE `runner_detail` (
    `runner_id` VARCHAR(36) NOT NULL,
    `shoes_type` VARCHAR(255) NOT NULL,
    `ip_address` VARCHAR(255) NOT NULL,
    `target_id` VARCHAR(36) NOT NULL,
    `cloud_id` TEXT NOT NULL,
    `resource_type` ENUM('nano', 'micro', 'small', 'medium', 'large', 'xlarge', '2xlarge', '3xlarge', '4xlarge') NOT NULL,
    `runner_user` VARCHAR(255),
    `provider_url` VARCHAR(255),
    `repository_url` VARCHAR(255) NOT NULL,
    `request_webhook` TEXT NOT NULL,
    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
    `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,
    KEY `fk_runner_target_id` (`target_id`),
    CONSTRAINT `runners_ibfk_1` FOREIGN KEY fk_runner_target_id(`target_id`) REFERENCES targets(`uuid`) ON DELETE RESTRICT,
    KEY `fk_runner_detail_id` (`runner_id`),
    CONSTRAINT `runners_ibfk_2` FOREIGN KEY fk_runner_detail_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE RESTRICT
);

CREATE TABLE `runners_running` (
    `runner_id` VARCHAR(36) NOT NULL,
    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
    KEY `fk_runner_deleted_id` (`runner_id`),
    CONSTRAINT `runners_running_ibfk_1` FOREIGN KEY fk_runner_deleted_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE CASCADE
);

CREATE TABLE `runners_deleted` (
    `runner_id` VARCHAR(36) NOT NULL,
    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
    `reason` VARCHAR(255) NOT NULL,
    KEY `fk_runner_deleted_id` (`runner_id`),
    CONSTRAINT `runners_deleted_ibfk_1` FOREIGN KEY fk_runner_deleted_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE CASCADE
);

CREATE TABLE `jobs` (
    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,
    `ghe_domain` VARCHAR(255),
    `repository` VARCHAR(255) NOT NULL,
    `check_event` TEXT NOT NULL,
    `target_id` VARCHAR(36) NOT NULL,
    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,
    `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,
    KEY `fk_job_target_id` (`target_id`),
    CONSTRAINT `jobs_ibfk_1` FOREIGN KEY fk_job_target_id(`target_id`) REFERENCES targets(`uuid`) ON DELETE RESTRICT
);


================================================
FILE: pkg/datastore/mysql/target.go
================================================
package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	uuid "github.com/satori/go.uuid"
	"github.com/whywaita/myshoes/pkg/datastore"
)

// CreateTarget create a target
func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Target) error {
	expiredAtRFC3339 := target.TokenExpiredAt.Format("2006-01-02 15:04:05")

	query := `INSERT INTO targets(uuid, scope, ghe_domain, github_token, token_expired_at, resource_type, provider_url) VALUES (?, ?, ?, ?, ?, ?, ?)`
	if _, err := m.Conn.ExecContext(
		ctx,
		query,
		target.UUID,
		target.Scope,
		target.GHEDomain,
		target.GitHubToken,
		expiredAtRFC3339,
		target.ResourceType,
		target.ProviderURL,
	); err != nil {
		return fmt.Errorf("failed to execute INSERT query: %w", err)
	}

	return nil
}

// GetTarget get a target
func (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) {
	var t datastore.Target
	query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?`
	if err := m.Conn.GetContext(ctx, &t, query, id.String()); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return &t, nil
}

// GetTargetByScope get a target from scope
func (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) {
	var t datastore.Target
	query := fmt.Sprintf(`SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE scope = "%s"`, scope)
	if err := m.Conn.GetContext(ctx, &t, query); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, datastore.ErrNotFound
		}

		return nil, fmt.Errorf("failed to execute SELECT query: %w", err)
	}

	return &t, nil
}

// ListTargets get a all target
func (m *MySQL) ListTargets(ctx context.Context) ([]datastore.Target, error) {
	var ts []datastore.Target
	query := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets`
	if err := m.Conn.SelectContext(ctx, &ts, query); err != nil {
		return nil, fmt.Errorf("failed to SELECT query: %w", err)
	}

	return ts, nil
}

// DeleteTarget delete a target
func (m *MySQL) DeleteTarget(ctx context.Context, id uuid.UUID) error {
	query := `UPDATE targets SET status = "deleted" WHERE uuid = ?`
	if _, err := m.Conn.ExecContext(ctx, query, id.String()); err != nil {
		return fmt.Errorf("failed to execute DELETE query: %w", err)
	}

	return nil
}

// UpdateTargetStatus update status in target
func (m *MySQL) UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus datastore.TargetStatus, description string) error {
	query := `UPDATE targets SET status = ?, status_description = ? WHERE uuid = ?`
	if _, err := m.Conn.ExecContext(ctx, query, newStatus, description, targetID.String()); err != nil {
		return fmt.Errorf("failed to execute UPDATE query: %w", err)
	}

	return nil
}

// UpdateToken update token in target
func (m *MySQL) UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error {
	query := `UPDATE targets SET github_token = ?, token_expired_at = ? WHERE uuid = ?`
	if _, err := m.Conn.ExecContext(ctx, query, newToken, newExpiredAt, targetID.String()); err != nil {
		return fmt.Errorf("failed to execute UPDATE query: %w", err)
	}

	return nil
}

// UpdateTargetParam update parameter of target
func (m *MySQL) UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType datastore.ResourceType, newProviderURL sql.NullString) error {
	query := `UPDATE targets SET resource_type = ?, provider_url = ? WHERE uuid = ?`
	if _, err := m.Conn.ExecContext(ctx, query, newResourceType, newProviderURL, targetID.String()); err != nil {
		return fmt.Errorf("failed to execute UPDATE query: %w", err)
	}

	return nil
}


================================================
FILE: pkg/datastore/mysql/target_test.go
================================================
package mysql_test

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/jmoiron/sqlx"
	uuid "github.com/satori/go.uuid"

	"github.com/whywaita/myshoes/internal/testutils"
	"github.com/whywaita/myshoes/pkg/datastore"
)

var testTargetID = uuid.FromStringOrNil("8a72d42c-372c-4e0d-9c6a-4304d44af137")
var testTargetID2 = uuid.FromStringOrNil("d14ccfea-b123-4ada-974e-bbff0937e9c7")
var testScopeOrg = "octocat"
var testScopeRepo = "octocat/hello-world"
var testScopeRepo2 = "octocat/hello-world2"
var testGitHubToken = "this-code-is-github-token"
var testRunnerUser = "testing-super-user"
var testProviderURL = "/shoes-mock"
var testTime = time.Date(2037, 9, 3, 0, 0, 0, 0, time.UTC)

func TestMySQL_CreateTarget(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	tests := []struct {
		input datastore.Target
		want  *datastore.Target
		err   bool
	}{
		{
			input: datastore.Target{
				UUID:           testTargetID,
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			want: &datastore.Target{
				UUID:           testTargetID,
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				Status:         datastore.TargetStatusActive,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			err: false,
		},
		{
			input: datastore.Target{
				UUID:           testTargetID2,
				Scope:          testScopeRepo2,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				GHEDomain: sql.NullString{
					String: "https://example.com",
					Valid:  true,
				},
				ResourceType: datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			want: &datastore.Target{
				UUID:           testTargetID2,
				Scope:          testScopeRepo2,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				GHEDomain: sql.NullString{
					String: "https://example.com",
					Valid:  true,
				},
				Status:       datastore.TargetStatusActive,
				ResourceType: datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		err := testDatastore.CreateTarget(context.Background(), test.input)
		if !test.err && err != nil {
			t.Fatalf("failed to create target: %+v", err)
		}
		got, err := getTargetFromSQL(testDB, test.input.UUID)
		if err != nil {
			t.Fatalf("failed to get target from SQL: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_GetTarget(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
		ProviderURL: sql.NullString{
			String: testProviderURL,
			Valid:  true,
		},
	})
	if err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	tests := []struct {
		input uuid.UUID
		want  *datastore.Target
		err   bool
	}{
		{
			input: testTargetID,
			want: &datastore.Target{
				UUID:           testTargetID,
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				Status:         datastore.TargetStatusActive,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		got, err := testDatastore.GetTarget(context.Background(), test.input)
		if err != nil {
			t.Fatalf("failed to get target: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_GetTargetByScope(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	tests := []struct {
		input   string
		want    *datastore.Target
		prepare func() error
		err     bool
	}{
		{
			// create single instance
			input: testScopeRepo,
			want: &datastore.Target{
				UUID:           testTargetID,
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				Status:         datastore.TargetStatusActive,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			prepare: func() error {
				return testDatastore.CreateTarget(context.Background(), datastore.Target{
					UUID:           testTargetID,
					Scope:          testScopeRepo,
					GitHubToken:    testGitHubToken,
					TokenExpiredAt: testTime,
					ResourceType:   datastore.ResourceTypeNano,
					ProviderURL: sql.NullString{
						String: testProviderURL,
						Valid:  true,
					},
				})
			},
			err: false,
		},
		{
			// repository is active and organization is deleted, correct return repository
			input: testScopeRepo,
			want: &datastore.Target{
				UUID:           testTargetID,
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				Status:         datastore.TargetStatusActive,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			prepare: func() error {
				if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
					UUID:           testTargetID,
					Scope:          testScopeRepo,
					GitHubToken:    testGitHubToken,
					TokenExpiredAt: testTime,
					ResourceType:   datastore.ResourceTypeNano,
					ProviderURL: sql.NullString{
						String: testProviderURL,
						Valid:  true,
					},
				}); err != nil {
					return fmt.Errorf("failed to create repository: %w", err)
				}

				if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
					UUID:           testTargetID2,
					Scope:          testScopeOrg,
					GitHubToken:    testGitHubToken,
					TokenExpiredAt: testTime,
					ResourceType:   datastore.ResourceTypeNano,
					ProviderURL: sql.NullString{
						String: testProviderURL,
						Valid:  true,
					},
				}); err != nil {
					return fmt.Errorf("failed to create organization (will delete): %w", err)
				}

				if err := testDatastore.DeleteTarget(context.Background(), testTargetID2); err != nil {
					return fmt.Errorf("failed to delete organization: %w", err)
				}

				return nil
			},
			err: false,
		},
		{
			// repository is deleted and organization is active, correct return organization
			input: testScopeOrg,
			want: &datastore.Target{
				UUID:           testTargetID2,
				Scope:          testScopeOrg,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				Status:         datastore.TargetStatusActive,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			prepare: func() error {
				if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
					UUID:           testTargetID,
					Scope:          testScopeRepo,
					GitHubToken:    testGitHubToken,
					TokenExpiredAt: testTime,
					ResourceType:   datastore.ResourceTypeNano,
					ProviderURL: sql.NullString{
						String: testProviderURL,
						Valid:  true,
					},
				}); err != nil {
					return fmt.Errorf("failed to create repository (will delete): %w", err)
				}

				if err := testDatastore.DeleteTarget(context.Background(), testTargetID); err != nil {
					return fmt.Errorf("failed to delete repository: %w", err)
				}

				if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
					UUID:           testTargetID2,
					Scope:          testScopeOrg,
					GitHubToken:    testGitHubToken,
					TokenExpiredAt: testTime,
					ResourceType:   datastore.ResourceTypeNano,
					ProviderURL: sql.NullString{
						String: testProviderURL,
						Valid:  true,
					},
				}); err != nil {
					return fmt.Errorf("failed to create deleted organization: %w", err)
				}

				return nil
			},
			err: false,
		},
	}

	for _, test := range tests {
		if err := test.prepare(); err != nil {
			t.Fatalf("failed to prepare function: %+v", err)
		}

		got, err := testDatastore.GetTargetByScope(context.Background(), test.input)
		if err != nil {
			t.Fatalf("failed to get target: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}

		teardown()
	}
}

func TestMySQL_ListTargets(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
		ProviderURL: sql.NullString{
			String: testProviderURL,
			Valid:  true,
		},
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	tests := []struct {
		input interface{}
		want  []datastore.Target
		err   bool
	}{
		{
			input: nil,
			want: []datastore.Target{
				{
					UUID:           testTargetID,
					Scope:          testScopeRepo,
					GitHubToken:    testGitHubToken,
					TokenExpiredAt: testTime,
					Status:         datastore.TargetStatusActive,
					ResourceType:   datastore.ResourceTypeNano,
					ProviderURL: sql.NullString{
						String: testProviderURL,
						Valid:  true,
					},
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		got, err := testDatastore.ListTargets(context.Background())
		if !test.err && err != nil {
			t.Fatalf("failed to list targets: %+v", err)
		}
		for i := range got {
			got[i].CreatedAt = time.Time{}
			got[i].UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_DeleteTarget(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
		UUID:           testTargetID,
		Scope:          testScopeRepo,
		GitHubToken:    testGitHubToken,
		TokenExpiredAt: testTime,
		ResourceType:   datastore.ResourceTypeNano,
		ProviderURL: sql.NullString{
			String: testProviderURL,
			Valid:  true,
		},
	}); err != nil {
		t.Fatalf("failed to create target: %+v", err)
	}

	tests := []struct {
		input uuid.UUID
		want  *datastore.Target
		err   bool
	}{
		{
			input: testTargetID,
			want: &datastore.Target{
				UUID:           testTargetID,
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
				Status: datastore.TargetStatusDeleted,
			},
			err: false,
		},
	}

	for _, test := range tests {
		err := testDatastore.DeleteTarget(context.Background(), test.input)
		if !test.err && err != nil {
			t.Fatalf("failed to delete target: %+v", err)
		}
		got, err := getTargetFromSQL(testDB, test.input)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			t.Fatalf("failed to get target from SQL: %+v", err)
		}
		if got != nil {
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}
	}
}

func TestMySQL_UpdateStatus(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	type Input struct {
		status      datastore.TargetStatus
		description string
	}

	tests := []struct {
		input Input
		want  *datastore.Target
		err   bool
	}{
		{
			input: Input{
				status:      datastore.TargetStatusActive,
				description: "",
			},
			want: &datastore.Target{
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
				Status: datastore.TargetStatusActive,
				StatusDescription: sql.NullString{
					String: "",
					Valid:  true,
				},
			},
			err: false,
		},
		{
			input: Input{
				status:      datastore.TargetStatusRunning,
				description: "job-id",
			},
			want: &datastore.Target{
				Scope:          testScopeRepo,
				GitHubToken:    testGitHubToken,
				TokenExpiredAt: testTime,
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
				Status: datastore.TargetStatusRunning,
				StatusDescription: sql.NullString{
					String: "job-id",
					Valid:  true,
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		tID := uuid.NewV4()
		if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
			UUID:           tID,
			Scope:          testScopeRepo,
			GitHubToken:    testGitHubToken,
			TokenExpiredAt: testTime,
			ResourceType:   datastore.ResourceTypeNano,
			ProviderURL: sql.NullString{
				String: testProviderURL,
				Valid:  true,
			},
		}); err != nil {
			t.Fatalf("failed to create target: %+v", err)
		}

		//lint:ignore SA1019 only execute in test
		err := testDatastore.UpdateTargetStatus(context.Background(), tID, test.input.status, test.input.description)
		if !test.err && err != nil {
			t.Fatalf("failed to update status: %+v", err)
		}
		got, err := getTargetFromSQL(testDB, tID)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			t.Fatalf("failed to get target from SQL: %+v", err)
		}
		if got != nil {
			got.UUID = uuid.UUID{}
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}

		if err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {
			t.Fatalf("failed to delete target: %+v", err)
		}
	}
}

func TestMySQL_UpdateToken(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	type Input struct {
		token   string
		expired time.Time
	}

	tests := []struct {
		input Input
		want  *datastore.Target
		err   bool
	}{
		{
			input: Input{
				token:   "new-token",
				expired: testTime.Add(1 * time.Hour),
			},
			want: &datastore.Target{
				Scope:          testScopeRepo,
				GitHubToken:    "new-token",
				TokenExpiredAt: testTime.Add(1 * time.Hour),
				ResourceType:   datastore.ResourceTypeNano,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
				Status: datastore.TargetStatusActive,
				StatusDescription: sql.NullString{
					String: "",
					Valid:  false,
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		tID := uuid.NewV4()
		if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
			UUID:           tID,
			Scope:          testScopeRepo,
			GitHubToken:    testGitHubToken,
			TokenExpiredAt: testTime,
			ResourceType:   datastore.ResourceTypeNano,
			ProviderURL: sql.NullString{
				String: testProviderURL,
				Valid:  true,
			},
		}); err != nil {
			t.Fatalf("failed to create target: %+v", err)
		}

		err := testDatastore.UpdateToken(context.Background(), tID, test.input.token, test.input.expired)
		if !test.err && err != nil {
			t.Fatalf("failed to update status: %+v", err)
		}
		got, err := getTargetFromSQL(testDB, tID)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			t.Fatalf("failed to get target from SQL: %+v", err)
		}
		if got != nil {
			got.UUID = uuid.UUID{}
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}

		if err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {
			t.Fatalf("failed to delete target: %+v", err)
		}
	}
}

func TestMySQL_UpdateTargetParam(t *testing.T) {
	testDatastore, teardown := testutils.GetTestDatastore()
	defer teardown()
	testDB, _ := testutils.GetTestDB()

	type input struct {
		resourceType datastore.ResourceType
		runnerUser   sql.NullString
		providerURL  sql.NullString
	}

	tests := []struct {
		input input
		want  *datastore.Target
		err   bool
	}{
		{
			input: input{
				resourceType: datastore.ResourceTypeLarge,
				runnerUser: sql.NullString{
					String: "",
					Valid:  false,
				},
				providerURL: sql.NullString{
					String: "",
					Valid:  false,
				},
			},
			want: &datastore.Target{
				Scope:        testScopeRepo,
				GitHubToken:  testGitHubToken,
				ResourceType: datastore.ResourceTypeLarge,
				ProviderURL: sql.NullString{
					String: "",
					Valid:  false,
				},
				Status: datastore.TargetStatusActive,
				StatusDescription: sql.NullString{
					String: "",
					Valid:  false,
				},
			},
			err: false,
		},
		{
			input: input{
				resourceType: datastore.ResourceTypeLarge,
				runnerUser: sql.NullString{
					String: testRunnerUser,
					Valid:  true,
				},
				providerURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
			},
			want: &datastore.Target{
				Scope:        testScopeRepo,
				GitHubToken:  testGitHubToken,
				ResourceType: datastore.ResourceTypeLarge,
				ProviderURL: sql.NullString{
					String: testProviderURL,
					Valid:  true,
				},
				Status: datastore.TargetStatusActive,
				StatusDescription: sql.NullString{
					String: "",
					Valid:  false,
				},
			},
			err: false,
		},
		{
			input: input{
				resourceType: datastore.ResourceTypeLarge,
				runnerUser: sql.NullString{
					String: testRunnerUser,
					Valid:  true,
				},
				providerURL: sql.NullString{
					String: "",
					Valid:  false,
				},
			},
			want: &datastore.Target{
				Scope:        testScopeRepo,
				GitHubToken:  testGitHubToken,
				ResourceType: datastore.ResourceTypeLarge,
				ProviderURL: sql.NullString{
					String: "",
					Valid:  false,
				},
				Status: datastore.TargetStatusActive,
				StatusDescription: sql.NullString{
					String: "",
					Valid:  false,
				},
			},
			err: false,
		},
	}

	for _, test := range tests {
		tID := uuid.NewV4()
		if err := testDatastore.CreateTarget(context.Background(), datastore.Target{
			UUID:           tID,
			Scope:          testScopeRepo,
			GitHubToken:    testGitHubToken,
			TokenExpiredAt: testTime,
			ResourceType:   datastore.ResourceTypeNano,
			ProviderURL: sql.NullString{
				String: "test-default-string",
				Valid:  true,
			},
		}); err != nil {
			t.Fatalf("failed to create target: %+v", err)
		}

		if err := testDatastore.UpdateTargetParam(context.Background(), tID, test.input.resourceType, test.input.providerURL); err != nil {
			t.Fatalf("failed to UpdateResourceTyoe: %+v", err)
		}

		got, err := getTargetFromSQL(testDB, tID)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			t.Fatalf("failed to get target from SQL: %+v", err)
		}
		if got != nil {
			got.UUID = uuid.UUID{}
			got.CreatedAt = time.Time{}
			got.UpdatedAt = time.Time{}
			got.TokenExpiredAt = time.Time{}
		}

		if diff := cmp.Diff(test.want, got); diff != "" {
			t.Errorf("mismatch (-want +got):\n%s", diff)
		}

		if err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {
			t.Fatalf("failed to delete target: %+v", err)
		}
	}
}

func getTargetFromSQL(testDB *sqlx.DB, uuid uuid.UUID) (*datastore.Target, error) {
	var t datastore.Target
	query := `SELECT uuid, scope, ghe_domain, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?`
	stmt, err := testDB.Preparex(query)
	if err != nil {
		return nil, fmt.Errorf("failed to prepare: %w", err)
	}
	err = stmt.Get(&t, uuid)
	if err != nil {
		return nil, fmt.Errorf("failed to get target: %w", err)
	}
	return &t, nil
}


================================================
FILE: pkg/datastore/resource_type.go
================================================
package datastore

import (
	"database/sql/driver"
	"encoding/json"
	"fmt"

	pb "github.com/whywaita/myshoes/api/proto.go"
)

// ResourceType is runner machine spec
type ResourceType int

// ResourceTypes variables
const (
	ResourceTypeUnknown ResourceType = iota
	ResourceTypeNano
	ResourceTypeMicro
	ResourceTypeSmall
	ResourceTypeMedium
	ResourceTypeLarge
	ResourceTypeXLarge
	ResourceType2XLarge
	ResourceType3XLarge
	ResourceType4XLarge
)

// String implement interface for fmt.Stringer
func (r ResourceType) String() string {
	switch r {
	case ResourceTypeNano:
		return "nano"
	case ResourceTypeMicro:
		return "micro"
	case ResourceTypeSmall:
		return "small"
	case ResourceTypeMedium:
		return "medium"
	case ResourceTypeLarge:
		return "large"
	case ResourceTypeXLarge:
		return "xlarge"
	case ResourceType2XLarge:
		return "2xlarge"
	case ResourceType3XLarge:
		return "3xlarge"
	case ResourceType4XLarge:
		return "4xlarge"
	}

	return "unknown"
}

// Value implements the database/sql/driver Valuer interface
func (r ResourceType) Value() (driver.Value, error) {
	return driver.Value(r.String()), nil
}

// Scan implements the database/sql Scanner interface
func (r *ResourceType) Scan(src interface{}) error {
	var rt *ResourceType
	switch src := src.(type) {
	case string:
		unmarshaled := UnmarshalResourceType(src)
		rt = &unmarshaled
	case []uint8:
		str := string(src)
		unmarshaled := UnmarshalResourceType(str)
		rt = &unmarshaled
	default:
		return fmt.Errorf("incompatible type for ResourceType: %T", src)
	}

	*r = *rt
	return nil
}

// UnmarshalResourceType cast type to ResourceType
func UnmarshalResourceType(src interface{}) ResourceType {
	switch src := src.(type) {
	case string:
		return UnmarshalResourceTypeString(src)
	case pb.ResourceType:
		return UnmarshalResourceTypePb(src)
	}

	return ResourceTypeUnknown
}

// UnmarshalResourceTypeString cast type from string to ResourceType
func UnmarshalResourceTypeString(in string) ResourceType {
	switch in {
	case "nano":
		return ResourceTypeNano
	case "micro":
		return ResourceTypeMicro
	case "small":
		return ResourceTypeSmall
	case "medium":
		return ResourceTypeMedium
	case "large":
		return ResourceTypeLarge
	case "xlarge":
		return ResourceTypeXLarge
	case "2xlarge":
		return ResourceType2XLarge
	case "3xlarge":
		return ResourceType3XLarge
	case "4xlarge":
		return ResourceType4XLarge
	}

	return ResourceTypeUnknown
}

// UnmarshalResourceTypePb cast type from pb.ResourceType to ResourceType
func UnmarshalResourceTypePb(in pb.ResourceType) ResourceType {
	switch in {
	case pb.ResourceType_Nano:
		return ResourceTypeNano
	case pb.ResourceType_Micro:
		return ResourceTypeMicro
	case pb.ResourceType_Small:
		return ResourceTypeSmall
	case pb.ResourceType_Medium:
		return ResourceTypeMedium
	case pb.ResourceType_Large:
		return ResourceTypeLarge
	case pb.ResourceType_XLarge:
		return ResourceTypeXLarge
	case pb.ResourceType_XLarge2:
		return ResourceType2XLarge
	case pb.ResourceType_XLarge3:
		return ResourceType3XLarge
	case pb.ResourceType_XLarge4:
		return ResourceType4XLarge
	}

	return ResourceTypeUnknown
}

// ToPb convert type of protobuf
func (r ResourceType) ToPb() pb.ResourceType {
	switch r {
	case ResourceTypeNano:
		return pb.ResourceType_Nano
	case ResourceTypeMicro:
		return pb.ResourceType_Micro
	case ResourceTypeSmall:
		return pb.ResourceType_Small
	case ResourceTypeMedium:
		return pb.ResourceType_Medium
	case ResourceTypeLarge:
		return pb.ResourceType_Large
	case ResourceTypeXLarge:
		return pb.ResourceType_XLarge
	case ResourceType2XLarge:
		return pb.ResourceType_XLarge2
	case ResourceType3XLarge:
		return pb.ResourceType_XLarge3
	case ResourceType4XLarge:
		return pb.ResourceType_XLarge4
	}

	return pb.ResourceType_Unknown
}

// MarshalJSON implements the encoding/json Marshaler interface
func (r ResourceType) MarshalJSON() ([]byte, error) {
	return json.Marshal(r.String())
}

// UnmarshalJSON implements the encoding/json Unmarshaler interface
func (r *ResourceType) UnmarshalJSON(data []byte) error {
	var s string
	if err := json.Unmarshal(data, &s); err != nil {
		return fmt.Errorf("data should be a string, but got %s", data)
	}

	rt := UnmarshalResourceTypeString(s)
	*r = rt
	return nil
}


================================================
FILE: pkg/docker/ratelimit.go
================================================
package docker

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"github.com/whywaita/myshoes/pkg/config"
)

// RateLimit is Docker Hub API rate limit
type RateLimit struct {
	Limit     int
	Remaining int
}

type tokenCache struct {
	expire time.Time
	token  string
}

var cacheMap = make(map[int]tokenCache, 1)

func getToken() (string, error) {
	url := "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull"
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", fmt.Errorf("create request: %w", err)
	}
	if config.Config.DockerHubCredential.Password != "" && config.Config.DockerHubCredential.Username != "" {
		req.SetBasicAuth(config.Config.DockerHubCredential.Username, config.Config.DockerHubCredential.Password)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("request token: %w", err)
	}
	if cache, ok := cacheMap[0]; ok && cache.expire.After(time.Now()) {
		return cache.token, nil
	}
	defer resp.Body.Close()
	byteArray, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("read body: %w", err)
	}
	jsonMap := make(map[string]interface{})
	if err := json.Unmarshal(byteArray, &jsonMap); err != nil {
		return "", fmt.Errorf("unmarshal json: %w", err)
	}
	tokenString, ok := jsonMap["token"].(string)
	if !ok {
		return "", fmt.Errorf("tokenString is not string")
	}
	token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
	if err != nil {
		return "", fmt.Errorf("parse token: %w", err)
	}
	exp, ok := token.Claims.(jwt.MapClaims)["exp"].(float64)
	if !ok {
		return "", fmt.Errorf("exp is not float64")
	}
	cacheMap[0] = tokenCache{
		expire: time.Unix(int64(exp), 0),
		token:  tokenString,
	}
	return tokenString, nil
}

// GetRateLimit get Docker Hub API rate limit
func GetRateLimit() (RateLimit, error) {
	token, err := getToken()
	if err != nil {
		return RateLimit{}, fmt.Errorf("get token: %w", err)
	}
	url := "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest"
	req, err := http.NewRequest("HEAD", url, nil)
	if err != nil {
		return RateLimit{}, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return RateLimit{}, fmt.Errorf("get rate limit: %w", err)
	}
	defer resp.Body.Close()
	limitHeader := resp.Header.Get("ratelimit-limit")
	if limitHeader == "" {
		return RateLimit{}, fmt.Errorf("not found ratelimit-limit header")
	}
	limit, err := strconv.Atoi(strings.Split(limitHeader, ";")[0])
	if err != nil {
		return RateLimit{}, fmt.Errorf("parse limit: %w", err)
	}
	remainingHeader := resp.Header.Get("ratelimit-remaining")
	if remainingHeader == "" {
		return RateLimit{}, fmt.Errorf("not found ratelimit-remaining header")

	}
	remaining, err := strconv.Atoi(strings.Split(remainingHeader, ";")[0])
	if err != nil {
		return RateLimit{}, fmt.Errorf("parse remaining: %w", err)
	}

	return RateLimit{
		Limit:     limit,
		Remaining: remaining,
	}, nil
}


================================================
FILE: pkg/gh/github.go
================================================
package gh

import (
	"fmt"
	"net/http"
	"net/url"
	"path"
	"sync"
	"time"

	"github.com/bradleyfalzon/ghinstallation/v2"
	"github.com/google/go-github/v80/github"
	"github.com/m4ns0ur/httpcache"
	"github.com/patrickmn/go-cache"
	"github.com/whywaita/myshoes/pkg/config"
	"golang.org/x/oauth2"
)

var (
	// ErrNotFound is error for not found
	ErrNotFound = fmt.Errorf("not found")

	// ResponseCache is cache variable
	responseCache = cache.New(5*time.Minute, 10*time.Minute)

	// rateLimitRemain is remaining of Rate limit, for metrics
	rateLimitRemain = sync.Map{}
	// rateLimitLimit is limit of Rate limit, for metrics
	rateLimitLimit = sync.Map{}

	// httpCache is shareable response cache
	httpCache = httpcache.NewMemoryCache()
	// appTransport is transport for GitHub Apps
	appTransport = ghinstallation.AppsTransport{}
	// installationTransports is map of ghinstallation.Transport for cache token of installation.
	// key: installationID, value: ghinstallation.Transport
	installationTransports = sync.Map{}
)

// InitializeCache create a cache
func InitializeCache(appID int64, appPEM []byte) error {
	tr := httpcache.NewTransport(httpCache)
	itr, err := ghinstallation.NewAppsTransport(tr, appID, appPEM)
	if err != nil {
		return fmt.Errorf("failed to create Apps transport: %w", err)
	}
	appTransport = *itr
	return nil
}

// NewClient create a client of GitHub
func NewClient(token string) (*github.Client, error) {
	oauth2Transport := &oauth2.Transport{
		Source: oauth2.StaticTokenSource(
			&oauth2.Token{AccessToken: token},
		),
	}
	transport := &httpcache.Transport{
		Transport:           oauth2Transport,
		Cache:               httpCache,
		MarkCachedResponses: true,
	}
	clientTransport := newInstrumentedTransport(transport)

	if !config.Config.IsGHES() {
		return github.NewClient(&http.Client{Transport: clientTransport}), nil
	}

	return github.NewClient(&http.Client{Transport: clientTransport}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)
}

// NewClientGitHubApps create a client of GitHub using Private Key from GitHub Apps
// header is "Authorization: Bearer YOUR_JWT"
// docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app
func NewClientGitHubApps() (*github.Client, error) {
	if !config.Config.IsGHES() {
		return github.NewClient(&http.Client{Transport: newInstrumentedTransport(&appTransport)}), nil
	}

	apiEndpoint, err := getAPIEndpoint()
	if err != nil {
		return nil, fmt.Errorf("failed to get GitHub API Endpoint: %w", err)
	}

	itr := appTransport
	itr.BaseURL = apiEndpoint.String()
	return github.NewClient(&http.Client{Transport: newInstrumentedTransport(&appTransport)}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)
}

// NewClientInstallation create a client of GitHub using installation ID from GitHub Apps
// header is "Authorization: token YOUR_INSTALLATION_ACCESS_TOKEN"
// docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-an-installation
func NewClientInstallation(installationID int64) (*github.Client, error) {
	itr := getInstallationTransport(installationID)

	if !config.Config.IsGHES() {
		return github.NewClient(&http.Client{Transport: newInstrumentedTransport(itr)}), nil
	}
	apiEndpoint, err := getAPIEndpoint()
	if err != nil {
		return nil, fmt.Errorf("failed to get GitHub API Endpoint: %w", err)
	}
	itr.BaseURL = apiEndpoint.String()
	return github.NewClient(&http.Client{Transport: newInstrumentedTransport(itr)}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)
}

func setInstallationTransport(installationID int64, itr ghinstallation.Transport) {
	installationTransports.Store(installationID, itr)
}

func getInstallationTransport(installationID int64) *ghinstallation.Transport {
	got, found := installationTransports.Load(installationID)
	if !found {
		return generateInstallationTransport(installationID)
	}

	itr, ok := got.(ghinstallation.Transport)
	if !ok {
		return generateInstallationTransport(installationID)
	}
	return &itr
}

func generateInstallationTransport(installationID int64) *ghinstallation.Transport {
	itr := ghinstallation.NewFromAppsTransport(&appTransport, installationID)
	setInstallationTransport(installationID, *itr)
	return itr
}

// CheckSignature check trust installation id from event.
func CheckSignature(installationID int64) error {
	if itr := ghinstallation.NewFromAppsTransport(&appTransport, installationID); itr == nil {
		return fmt.Errorf("failed to create GitHub installation")
	}

	return nil
}

// ExistRunnerReleases check exist of runner file
func ExistRunnerReleases(runnerVersion string) error {
	releasesURL := fmt.Sprintf("https://github.com/actions/runner/releases/tag/%s", runnerVersion)
	resp, err := http.Get(releasesURL)
	if err != nil {
		return fmt.Errorf("failed to GET from %s: %w", releasesURL, ErrNotFound)
	}

	if resp.StatusCode == http.StatusOK {
		return nil
	} else if resp.StatusCode == http.StatusNotFound {
		return ErrNotFound
	}

	return fmt.Errorf("invalid response code (%d)", resp.StatusCode)
}

// ExistGitHubRepository check exist of GitHub repository
func ExistGitHubRepository(scope string, accessToken string) error {
	repoURL, err := getRepositoryURL(scope)
	if err != nil {
		return fmt.Errorf("failed to get repository url: %w", err)
	}

	client := &http.Client{Transport: newInstrumentedTransport(http.DefaultTransport)}
	req, err := http.NewRequest(http.Meth
Download .txt
gitextract_98bm4mpl/

├── .github/
│   └── workflows/
│       ├── build-docker-sha.yaml
│       ├── release.yaml
│       └── test.yaml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api/
│   ├── myshoes/
│   │   ├── README.md
│   │   ├── client.go
│   │   ├── http.go
│   │   └── target.go
│   ├── proto/
│   │   └── myshoes.proto
│   └── proto.go/
│       ├── myshoes.pb.go
│       └── myshoes_grpc.pb.go
├── cmd/
│   ├── server/
│   │   └── cmd.go
│   └── shoes-tester/
│       └── main.go
├── docs/
│   ├── 01_01_for_admin_setup.md
│   ├── 01_02_for_admin_tips.md
│   ├── 02_01_for_user_setup.md
│   ├── 03_how-to-develop-shoes.md
│   └── assets/
│       └── myshoes.service
├── go.mod
├── go.sum
├── internal/
│   ├── testutils/
│   │   ├── mysql.go
│   │   ├── testutils.go
│   │   └── web.go
│   └── util/
│       └── util.go
└── pkg/
    ├── config/
    │   ├── config.go
    │   └── init.go
    ├── datastore/
    │   ├── github.go
    │   ├── interface.go
    │   ├── memory/
    │   │   └── memory.go
    │   ├── mysql/
    │   │   ├── job.go
    │   │   ├── job_test.go
    │   │   ├── lock.go
    │   │   ├── mysql.go
    │   │   ├── mysql_test.go
    │   │   ├── runner.go
    │   │   ├── runner_test.go
    │   │   ├── schema.sql
    │   │   ├── target.go
    │   │   └── target_test.go
    │   └── resource_type.go
    ├── docker/
    │   └── ratelimit.go
    ├── gh/
    │   ├── github.go
    │   ├── github_test.go
    │   ├── installation.go
    │   ├── jwt.go
    │   ├── jwt_test.go
    │   ├── label.go
    │   ├── metrics.go
    │   ├── metrics_test.go
    │   ├── ratelimit.go
    │   ├── runner.go
    │   ├── scope.go
    │   ├── token_registration.go
    │   ├── webhook.go
    │   ├── workflow_job.go
    │   └── workflow_run.go
    ├── logger/
    │   └── logger.go
    ├── metric/
    │   ├── collector.go
    │   ├── scrape_datastore.go
    │   ├── scrape_github.go
    │   ├── scrape_memory.go
    │   └── webhook.go
    ├── runner/
    │   ├── metrics.go
    │   ├── runner.go
    │   ├── runner_delete.go
    │   ├── runner_delete_ephemeral.go
    │   ├── runner_delete_once.go
    │   ├── token_update.go
    │   └── util.go
    ├── shoes/
    │   └── shoes.go
    ├── starter/
    │   ├── README.md
    │   ├── error.go
    │   ├── metric.go
    │   ├── metrics.go
    │   ├── safety/
    │   │   ├── README.md
    │   │   ├── safety.go
    │   │   └── unlimited/
    │   │       └── unlimited.go
    │   ├── scripts/
    │   │   └── RunnerService.js
    │   ├── scripts.go
    │   └── starter.go
    └── web/
        ├── config.go
        ├── http.go
        ├── http_test.go
        ├── metrics.go
        ├── target.go
        ├── target_create.go
        ├── target_test.go
        └── webhook.go
Download .txt
SYMBOL INDEX (505 symbols across 69 files)

FILE: api/myshoes/client.go
  type Client (line 15) | type Client struct
    method newRequest (line 57) | func (c *Client) newRequest(ctx context.Context, method, spath string,...
  constant defaultUserAgent (line 24) | defaultUserAgent = "myshoes-sdk-go"
  function NewClient (line 28) | func NewClient(endpoint string, client *http.Client, logger *log.Logger)...

FILE: api/myshoes/http.go
  function decodeBody (line 12) | func decodeBody(resp *http.Response, out interface{}) error {
  function decodeErrorBody (line 26) | func decodeErrorBody(resp *http.Response) error {
  method request (line 36) | func (c *Client) request(req *http.Request, out interface{}) error {

FILE: api/myshoes/target.go
  method CreateTarget (line 14) | func (c *Client) CreateTarget(ctx context.Context, param web.TargetCreat...
  method GetTarget (line 36) | func (c *Client) GetTarget(ctx context.Context, targetID string) (*web.U...
  method UpdateTarget (line 53) | func (c *Client) UpdateTarget(ctx context.Context, targetID string, para...
  method DeleteTarget (line 75) | func (c *Client) DeleteTarget(ctx context.Context, targetID string) error {
  method ListTarget (line 92) | func (c *Client) ListTarget(ctx context.Context) ([]web.UserTarget, erro...

FILE: api/proto.go/myshoes.pb.go
  constant _ (line 19) | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  constant _ (line 21) | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  type ResourceType (line 24) | type ResourceType
    method Enum (line 67) | func (x ResourceType) Enum() *ResourceType {
    method String (line 73) | func (x ResourceType) String() string {
    method Descriptor (line 77) | func (ResourceType) Descriptor() protoreflect.EnumDescriptor {
    method Type (line 81) | func (ResourceType) Type() protoreflect.EnumType {
    method Number (line 85) | func (x ResourceType) Number() protoreflect.EnumNumber {
    method EnumDescriptor (line 90) | func (ResourceType) EnumDescriptor() ([]byte, []int) {
  constant ResourceType_Unknown (line 27) | ResourceType_Unknown ResourceType = 0
  constant ResourceType_Nano (line 28) | ResourceType_Nano    ResourceType = 1
  constant ResourceType_Micro (line 29) | ResourceType_Micro   ResourceType = 2
  constant ResourceType_Small (line 30) | ResourceType_Small   ResourceType = 3
  constant ResourceType_Medium (line 31) | ResourceType_Medium  ResourceType = 4
  constant ResourceType_Large (line 32) | ResourceType_Large   ResourceType = 5
  constant ResourceType_XLarge (line 33) | ResourceType_XLarge  ResourceType = 6
  constant ResourceType_XLarge2 (line 34) | ResourceType_XLarge2 ResourceType = 7
  constant ResourceType_XLarge3 (line 35) | ResourceType_XLarge3 ResourceType = 8
  constant ResourceType_XLarge4 (line 36) | ResourceType_XLarge4 ResourceType = 9
  type AddInstanceRequest (line 94) | type AddInstanceRequest struct
    method Reset (line 104) | func (x *AddInstanceRequest) Reset() {
    method String (line 111) | func (x *AddInstanceRequest) String() string {
    method ProtoMessage (line 115) | func (*AddInstanceRequest) ProtoMessage() {}
    method ProtoReflect (line 117) | func (x *AddInstanceRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 130) | func (*AddInstanceRequest) Descriptor() ([]byte, []int) {
    method GetRunnerName (line 134) | func (x *AddInstanceRequest) GetRunnerName() string {
    method GetSetupScript (line 141) | func (x *AddInstanceRequest) GetSetupScript() string {
    method GetResourceType (line 148) | func (x *AddInstanceRequest) GetResourceType() ResourceType {
    method GetLabels (line 155) | func (x *AddInstanceRequest) GetLabels() []string {
  type AddInstanceResponse (line 162) | type AddInstanceResponse struct
    method Reset (line 172) | func (x *AddInstanceResponse) Reset() {
    method String (line 179) | func (x *AddInstanceResponse) String() string {
    method ProtoMessage (line 183) | func (*AddInstanceResponse) ProtoMessage() {}
    method ProtoReflect (line 185) | func (x *AddInstanceResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 198) | func (*AddInstanceResponse) Descriptor() ([]byte, []int) {
    method GetCloudId (line 202) | func (x *AddInstanceResponse) GetCloudId() string {
    method GetShoesType (line 209) | func (x *AddInstanceResponse) GetShoesType() string {
    method GetIpAddress (line 216) | func (x *AddInstanceResponse) GetIpAddress() string {
    method GetResourceType (line 223) | func (x *AddInstanceResponse) GetResourceType() ResourceType {
  type DeleteInstanceRequest (line 230) | type DeleteInstanceRequest struct
    method Reset (line 238) | func (x *DeleteInstanceRequest) Reset() {
    method String (line 245) | func (x *DeleteInstanceRequest) String() string {
    method ProtoMessage (line 249) | func (*DeleteInstanceRequest) ProtoMessage() {}
    method ProtoReflect (line 251) | func (x *DeleteInstanceRequest) ProtoReflect() protoreflect.Message {
    method Descriptor (line 264) | func (*DeleteInstanceRequest) Descriptor() ([]byte, []int) {
    method GetCloudId (line 268) | func (x *DeleteInstanceRequest) GetCloudId() string {
    method GetLabels (line 275) | func (x *DeleteInstanceRequest) GetLabels() []string {
  type DeleteInstanceResponse (line 282) | type DeleteInstanceResponse struct
    method Reset (line 288) | func (x *DeleteInstanceResponse) Reset() {
    method String (line 295) | func (x *DeleteInstanceResponse) String() string {
    method ProtoMessage (line 299) | func (*DeleteInstanceResponse) ProtoMessage() {}
    method ProtoReflect (line 301) | func (x *DeleteInstanceResponse) ProtoReflect() protoreflect.Message {
    method Descriptor (line 314) | func (*DeleteInstanceResponse) Descriptor() ([]byte, []int) {
  constant file_myshoes_proto_rawDesc (line 320) | file_myshoes_proto_rawDesc = "" +
  function file_myshoes_proto_rawDescGZIP (line 362) | func file_myshoes_proto_rawDescGZIP() []byte {
  function init (line 392) | func init() { file_myshoes_proto_init() }
  function file_myshoes_proto_init (line 393) | func file_myshoes_proto_init() {

FILE: api/proto.go/myshoes_grpc.pb.go
  constant _ (line 19) | _ = grpc.SupportPackageIsVersion9
  constant Shoes_AddInstance_FullMethodName (line 22) | Shoes_AddInstance_FullMethodName    = "/whywaita.myshoes.Shoes/AddInstance"
  constant Shoes_DeleteInstance_FullMethodName (line 23) | Shoes_DeleteInstance_FullMethodName = "/whywaita.myshoes.Shoes/DeleteIns...
  type ShoesClient (line 29) | type ShoesClient interface
  type shoesClient (line 34) | type shoesClient struct
    method AddInstance (line 42) | func (c *shoesClient) AddInstance(ctx context.Context, in *AddInstance...
    method DeleteInstance (line 52) | func (c *shoesClient) DeleteInstance(ctx context.Context, in *DeleteIn...
  function NewShoesClient (line 38) | func NewShoesClient(cc grpc.ClientConnInterface) ShoesClient {
  type ShoesServer (line 65) | type ShoesServer interface
  type UnimplementedShoesServer (line 76) | type UnimplementedShoesServer struct
    method AddInstance (line 78) | func (UnimplementedShoesServer) AddInstance(context.Context, *AddInsta...
    method DeleteInstance (line 81) | func (UnimplementedShoesServer) DeleteInstance(context.Context, *Delet...
    method mustEmbedUnimplementedShoesServer (line 84) | func (UnimplementedShoesServer) mustEmbedUnimplementedShoesServer() {}
    method testEmbeddedByValue (line 85) | func (UnimplementedShoesServer) testEmbeddedByValue()               {}
  type UnsafeShoesServer (line 90) | type UnsafeShoesServer interface
  function RegisterShoesServer (line 94) | func RegisterShoesServer(s grpc.ServiceRegistrar, srv ShoesServer) {
  function _Shoes_AddInstance_Handler (line 105) | func _Shoes_AddInstance_Handler(srv interface{}, ctx context.Context, de...
  function _Shoes_DeleteInstance_Handler (line 123) | func _Shoes_DeleteInstance_Handler(srv interface{}, ctx context.Context,...

FILE: cmd/server/cmd.go
  function init (line 25) | func init() {
  function main (line 35) | func main() {
  type myShoes (line 52) | type myShoes struct
    method Run (line 80) | func (m *myShoes) Run() error {
  function newShoes (line 59) | func newShoes() (*myShoes, error) {

FILE: cmd/shoes-tester/main.go
  function main (line 23) | func main() {
  function printUsage (line 48) | func printUsage() {
  type addFlags (line 59) | type addFlags struct
  type deleteFlags (line 76) | type deleteFlags struct
  function runAdd (line 83) | func runAdd(args []string) error {
  function runDelete (line 177) | func runDelete(args []string) error {
  function getClientWithPath (line 225) | func getClientWithPath(pluginPath string) (shoes.Client, func(), error) {
  function generateSetupScript (line 259) | func generateSetupScript(ctx context.Context, flags *addFlags) (string, ...
  function parseLabels (line 301) | func parseLabels(labels string) []string {

FILE: internal/testutils/mysql.go
  constant schemaDirRelativePathFormat (line 16) | schemaDirRelativePathFormat = "%s/../../pkg/datastore/mysql/%s"
  function execSchema (line 18) | func execSchema(fpath string) {
  function createTablesIfNotExist (line 34) | func createTablesIfNotExist() {
  function truncateTables (line 40) | func truncateTables() {
  function GetTestDatastore (line 81) | func GetTestDatastore() (datastore.Datastore, func()) {
  function GetTestDB (line 90) | func GetTestDB() (*sqlx.DB, func()) {

FILE: internal/testutils/testutils.go
  constant mysqlRootPassword (line 18) | mysqlRootPassword = "secret"
  function IntegrationTestRunner (line 29) | func IntegrationTestRunner(m *testing.M) int {

FILE: internal/testutils/web.go
  function GetTestURL (line 4) | func GetTestURL() string {

FILE: internal/util/util.go
  function CalcRetryTime (line 9) | func CalcRetryTime(count int) time.Duration {

FILE: pkg/config/config.go
  type Conf (line 12) | type Conf struct
    method IsGHES (line 122) | func (c Conf) IsGHES() bool {
  type DockerHubCredential (line 37) | type DockerHubCredential struct
  type GitHubApp (line 43) | type GitHubApp struct
  constant EnvGitHubAppID (line 52) | EnvGitHubAppID               = "GITHUB_APP_ID"
  constant EnvGitHubAppSecret (line 53) | EnvGitHubAppSecret           = "GITHUB_APP_SECRET"
  constant EnvGitHubAppPrivateKeyBase64 (line 54) | EnvGitHubAppPrivateKeyBase64 = "GITHUB_PRIVATE_KEY_BASE64"
  constant EnvMySQLHost (line 55) | EnvMySQLHost                 = "MYSQL_HOST"
  constant EnvMySQLPort (line 56) | EnvMySQLPort                 = "MYSQL_PORT"
  constant EnvMySQLUser (line 57) | EnvMySQLUser                 = "MYSQL_USER"
  constant EnvMySQLPassword (line 58) | EnvMySQLPassword             = "MYSQL_PASSWORD"
  constant EnvMySQLDatabase (line 59) | EnvMySQLDatabase             = "MYSQL_DATABASE"
  constant EnvMySQLURL (line 60) | EnvMySQLURL                  = "MYSQL_URL"
  constant EnvPort (line 61) | EnvPort                      = "PORT"
  constant EnvShoesPluginPath (line 62) | EnvShoesPluginPath           = "PLUGIN"
  constant EnvShoesPluginOutputPath (line 63) | EnvShoesPluginOutputPath     = "PLUGIN_OUTPUT"
  constant EnvRunnerUser (line 64) | EnvRunnerUser                = "RUNNER_USER"
  constant EnvRunnerBaseDirectory (line 65) | EnvRunnerBaseDirectory       = "RUNNER_BASE_DIRECTORY"
  constant EnvDebug (line 66) | EnvDebug                     = "DEBUG"
  constant EnvStrict (line 67) | EnvStrict                    = "STRICT"
  constant EnvModeWebhookType (line 68) | EnvModeWebhookType           = "MODE_WEBHOOK_TYPE"
  constant EnvMaxConnectionsToBackend (line 69) | EnvMaxConnectionsToBackend   = "MAX_CONNECTIONS_TO_BACKEND"
  constant EnvMaxConcurrencyDeleting (line 70) | EnvMaxConcurrencyDeleting    = "MAX_CONCURRENCY_DELETING"
  constant EnvGitHubURL (line 71) | EnvGitHubURL                 = "GITHUB_URL"
  constant EnvRunnerVersion (line 72) | EnvRunnerVersion             = "RUNNER_VERSION"
  constant EnvDockerHubUsername (line 73) | EnvDockerHubUsername         = "DOCKER_HUB_USERNAME"
  constant EnvDockerHubPassword (line 74) | EnvDockerHubPassword         = "DOCKER_HUB_PASSWORD"
  constant EnvProvideDockerHubMetrics (line 75) | EnvProvideDockerHubMetrics   = "PROVIDE_DOCKER_HUB_METRICS"
  type ModeWebhookType (line 79) | type ModeWebhookType
    method String (line 91) | func (mwt ModeWebhookType) String() string {
    method Equal (line 106) | func (mwt ModeWebhookType) Equal(in string) bool {
  constant ModeWebhookTypeUnknown (line 83) | ModeWebhookTypeUnknown ModeWebhookType = iota
  constant ModeWebhookTypeCheckRun (line 85) | ModeWebhookTypeCheckRun
  constant ModeWebhookTypeWorkflowJob (line 87) | ModeWebhookTypeWorkflowJob
  function marshalModeWebhookType (line 110) | func marshalModeWebhookType(in string) ModeWebhookType {

FILE: pkg/config/init.go
  function Load (line 21) | func Load() {
  function LoadWithDefault (line 34) | func LoadWithDefault() Conf {
  function LoadGitHubApps (line 162) | func LoadGitHubApps() *GitHubApp {
  function LoadMySQLURL (line 200) | func LoadMySQLURL() string {
  function LoadPluginPath (line 219) | func LoadPluginPath() string {
  function checkBinary (line 236) | func checkBinary(p string) (string, error) {
  function fetch (line 267) | func fetch(p string) (string, error) {
  function fetchHTTP (line 288) | func fetchHTTP(u *url.URL) (string, error) {

FILE: pkg/datastore/github.go
  function NewClientInstallationByRepo (line 19) | func NewClientInstallationByRepo(ctx context.Context, ds Datastore, repo...
  type PendingWorkflowRunWithTarget (line 39) | type PendingWorkflowRunWithTarget struct
  function GetPendingWorkflowRunByRecentRepositories (line 45) | func GetPendingWorkflowRunByRecentRepositories(ctx context.Context, ds D...
  function getPendingWorkflowRunByRecentRepositories (line 88) | func getPendingWorkflowRunByRecentRepositories(ctx context.Context, ds D...
  function getPendingRunByRepo (line 135) | func getPendingRunByRepo(ctx context.Context, client *github.Client, own...
  function getRecentRepositories (line 158) | func getRecentRepositories(ctx context.Context, ds Datastore) ([]string,...

FILE: pkg/datastore/interface.go
  type Datastore (line 30) | type Datastore interface
  type Target (line 60) | type Target struct
    method OwnerRepo (line 77) | func (t *Target) OwnerRepo() (string, string) {
    method CanReceiveJob (line 82) | func (t *Target) CanReceiveJob() bool {
  function ListTargets (line 92) | func ListTargets(ctx context.Context, ds Datastore) ([]Target, error) {
  function UpdateTargetStatus (line 110) | func UpdateTargetStatus(ctx context.Context, ds Datastore, targetID uuid...
  function SearchRepo (line 131) | func SearchRepo(ctx context.Context, ds Datastore, repo string) (*Target...
  type TargetStatus (line 160) | type TargetStatus
  constant TargetStatusActive (line 164) | TargetStatusActive  TargetStatus = "active"
  constant TargetStatusRunning (line 165) | TargetStatusRunning              = "running"
  constant TargetStatusSuspend (line 166) | TargetStatusSuspend              = "suspend"
  constant TargetStatusDeleted (line 167) | TargetStatusDeleted              = "deleted"
  constant TargetStatusErr (line 168) | TargetStatusErr                  = "error"
  type Job (line 172) | type Job struct
    method RepoURL (line 183) | func (j *Job) RepoURL() string {
  type Runner (line 200) | type Runner struct
  type RunnerStatus (line 219) | type RunnerStatus
  constant RunnerStatusCreated (line 223) | RunnerStatusCreated        RunnerStatus = "created"
  constant RunnerStatusCompleted (line 224) | RunnerStatusCompleted                   = "completed"
  constant RunnerStatusReachHardLimit (line 225) | RunnerStatusReachHardLimit              = "reach_hard_limit"

FILE: pkg/datastore/memory/memory.go
  type Memory (line 16) | type Memory struct
    method CreateTarget (line 39) | func (m *Memory) CreateTarget(ctx context.Context, target datastore.Ta...
    method GetTarget (line 48) | func (m *Memory) GetTarget(ctx context.Context, id uuid.UUID) (*datast...
    method GetTargetByScope (line 60) | func (m *Memory) GetTargetByScope(ctx context.Context, scope string) (...
    method ListTargets (line 76) | func (m *Memory) ListTargets(ctx context.Context) ([]datastore.Target,...
    method DeleteTarget (line 90) | func (m *Memory) DeleteTarget(ctx context.Context, id uuid.UUID) error {
    method UpdateTargetStatus (line 99) | func (m *Memory) UpdateTargetStatus(ctx context.Context, targetID uuid...
    method UpdateToken (line 122) | func (m *Memory) UpdateToken(ctx context.Context, targetID uuid.UUID, ...
    method UpdateTargetParam (line 138) | func (m *Memory) UpdateTargetParam(ctx context.Context, targetID uuid....
    method EnqueueJob (line 157) | func (m *Memory) EnqueueJob(ctx context.Context, job datastore.Job) er...
    method ListJobs (line 166) | func (m *Memory) ListJobs(ctx context.Context) ([]datastore.Job, error) {
    method DeleteJob (line 179) | func (m *Memory) DeleteJob(ctx context.Context, id uuid.UUID) error {
    method CreateRunner (line 188) | func (m *Memory) CreateRunner(ctx context.Context, runner datastore.Ru...
    method ListRunners (line 198) | func (m *Memory) ListRunners(ctx context.Context) ([]datastore.Runner,...
    method ListRunnersByTargetID (line 211) | func (m *Memory) ListRunnersByTargetID(ctx context.Context, targetID u...
    method ListRunnersLogBySince (line 226) | func (m *Memory) ListRunnersLogBySince(ctx context.Context, since time...
    method GetRunner (line 241) | func (m *Memory) GetRunner(ctx context.Context, id uuid.UUID) (*datast...
    method DeleteRunner (line 254) | func (m *Memory) DeleteRunner(ctx context.Context, id uuid.UUID, delet...
    method GetLock (line 263) | func (m *Memory) GetLock(ctx context.Context) error {
    method IsLocked (line 268) | func (m *Memory) IsLocked(ctx context.Context) (string, error) {
  function New (line 24) | func New() (*Memory, error) {

FILE: pkg/datastore/mysql/job.go
  method EnqueueJob (line 14) | func (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error {
  method ListJobs (line 31) | func (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) {
  method DeleteJob (line 46) | func (m *MySQL) DeleteJob(ctx context.Context, id uuid.UUID) error {

FILE: pkg/datastore/mysql/job_test.go
  function TestMySQL_EnqueueJob (line 21) | func TestMySQL_EnqueueJob(t *testing.T) {
  function TestMySQL_ListJobs (line 80) | func TestMySQL_ListJobs(t *testing.T) {
  function TestMySQL_DeleteJob (line 149) | func TestMySQL_DeleteJob(t *testing.T) {
  function getJobFromSQL (line 209) | func getJobFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Job, error) {

FILE: pkg/datastore/mysql/lock.go
  method GetLock (line 13) | func (m *MySQL) GetLock(ctx context.Context) error {
  method IsLocked (line 31) | func (m *MySQL) IsLocked(ctx context.Context) (string, error) {

FILE: pkg/datastore/mysql/mysql.go
  type MySQL (line 12) | type MySQL struct
  function New (line 19) | func New(dsn string, notifyEnqueueCh chan<- struct{}) (*MySQL, error) {
  function getMySQLURL (line 36) | func getMySQLURL(dsn string) (string, error) {

FILE: pkg/datastore/mysql/mysql_test.go
  function TestMain (line 10) | func TestMain(m *testing.M) {

FILE: pkg/datastore/mysql/runner.go
  method CreateRunner (line 15) | func (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runne...
  method ListRunners (line 44) | func (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, er...
  method ListRunnersByTargetID (line 61) | func (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid...
  method ListRunnersLogBySince (line 78) | func (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Ti...
  method GetRunner (line 95) | func (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore...
  method DeleteRunner (line 111) | func (m *MySQL) DeleteRunner(ctx context.Context, id uuid.UUID, deletedA...

FILE: pkg/datastore/mysql/runner_test.go
  function TestMySQL_CreateRunner (line 21) | func TestMySQL_CreateRunner(t *testing.T) {
  function TestMySQL_ListRunners (line 123) | func TestMySQL_ListRunners(t *testing.T) {
  function TestMySQL_ListRunnersNotReturnDeleted (line 195) | func TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) {
  function TestMySQL_ListRunnersLogBySince (line 262) | func TestMySQL_ListRunnersLogBySince(t *testing.T) {
  function TestMySQL_GetRunner (line 326) | func TestMySQL_GetRunner(t *testing.T) {
  function TestMySQL_DeleteRunner (line 388) | func TestMySQL_DeleteRunner(t *testing.T) {
  function getRunnerFromSQL (line 465) | func getRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner,...
  function getRunningRunnerFromSQL (line 479) | func getRunningRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore....
  function getDeletedRunnerFromSQL (line 494) | func getDeletedRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore....

FILE: pkg/datastore/mysql/schema.sql
  type `targets` (line 1) | CREATE TABLE `targets` (
  type `runners` (line 16) | CREATE TABLE `runners` (
  type `runner_detail` (line 21) | CREATE TABLE `runner_detail` (
  type `runners_running` (line 40) | CREATE TABLE `runners_running` (
  type `runners_deleted` (line 47) | CREATE TABLE `runners_deleted` (
  type `jobs` (line 55) | CREATE TABLE `jobs` (

FILE: pkg/datastore/mysql/target.go
  method CreateTarget (line 15) | func (m *MySQL) CreateTarget(ctx context.Context, target datastore.Targe...
  method GetTarget (line 37) | func (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore...
  method GetTargetByScope (line 52) | func (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*da...
  method ListTargets (line 67) | func (m *MySQL) ListTargets(ctx context.Context) ([]datastore.Target, er...
  method DeleteTarget (line 78) | func (m *MySQL) DeleteTarget(ctx context.Context, id uuid.UUID) error {
  method UpdateTargetStatus (line 88) | func (m *MySQL) UpdateTargetStatus(ctx context.Context, targetID uuid.UU...
  method UpdateToken (line 98) | func (m *MySQL) UpdateToken(ctx context.Context, targetID uuid.UUID, new...
  method UpdateTargetParam (line 108) | func (m *MySQL) UpdateTargetParam(ctx context.Context, targetID uuid.UUI...

FILE: pkg/datastore/mysql/target_test.go
  function TestMySQL_CreateTarget (line 29) | func TestMySQL_CreateTarget(t *testing.T) {
  function TestMySQL_GetTarget (line 121) | func TestMySQL_GetTarget(t *testing.T) {
  function TestMySQL_GetTargetByScope (line 179) | func TestMySQL_GetTargetByScope(t *testing.T) {
  function TestMySQL_ListTargets (line 347) | func TestMySQL_ListTargets(t *testing.T) {
  function TestMySQL_DeleteTarget (line 406) | func TestMySQL_DeleteTarget(t *testing.T) {
  function TestMySQL_UpdateStatus (line 468) | func TestMySQL_UpdateStatus(t *testing.T) {
  function TestMySQL_UpdateToken (line 570) | func TestMySQL_UpdateToken(t *testing.T) {
  function TestMySQL_UpdateTargetParam (line 649) | func TestMySQL_UpdateTargetParam(t *testing.T) {
  function getTargetFromSQL (line 792) | func getTargetFromSQL(testDB *sqlx.DB, uuid uuid.UUID) (*datastore.Targe...

FILE: pkg/datastore/resource_type.go
  type ResourceType (line 12) | type ResourceType
    method String (line 29) | func (r ResourceType) String() string {
    method Value (line 55) | func (r ResourceType) Value() (driver.Value, error) {
    method Scan (line 60) | func (r *ResourceType) Scan(src interface{}) error {
    method ToPb (line 143) | func (r ResourceType) ToPb() pb.ResourceType {
    method MarshalJSON (line 169) | func (r ResourceType) MarshalJSON() ([]byte, error) {
    method UnmarshalJSON (line 174) | func (r *ResourceType) UnmarshalJSON(data []byte) error {
  constant ResourceTypeUnknown (line 16) | ResourceTypeUnknown ResourceType = iota
  constant ResourceTypeNano (line 17) | ResourceTypeNano
  constant ResourceTypeMicro (line 18) | ResourceTypeMicro
  constant ResourceTypeSmall (line 19) | ResourceTypeSmall
  constant ResourceTypeMedium (line 20) | ResourceTypeMedium
  constant ResourceTypeLarge (line 21) | ResourceTypeLarge
  constant ResourceTypeXLarge (line 22) | ResourceTypeXLarge
  constant ResourceType2XLarge (line 23) | ResourceType2XLarge
  constant ResourceType3XLarge (line 24) | ResourceType3XLarge
  constant ResourceType4XLarge (line 25) | ResourceType4XLarge
  function UnmarshalResourceType (line 79) | func UnmarshalResourceType(src interface{}) ResourceType {
  function UnmarshalResourceTypeString (line 91) | func UnmarshalResourceTypeString(in string) ResourceType {
  function UnmarshalResourceTypePb (line 117) | func UnmarshalResourceTypePb(in pb.ResourceType) ResourceType {

FILE: pkg/docker/ratelimit.go
  type RateLimit (line 17) | type RateLimit struct
  type tokenCache (line 22) | type tokenCache struct
  function getToken (line 29) | func getToken() (string, error) {
  function GetRateLimit (line 74) | func GetRateLimit() (RateLimit, error) {

FILE: pkg/gh/github.go
  function InitializeCache (line 41) | func InitializeCache(appID int64, appPEM []byte) error {
  function NewClient (line 52) | func NewClient(token string) (*github.Client, error) {
  function NewClientGitHubApps (line 75) | func NewClientGitHubApps() (*github.Client, error) {
  function NewClientInstallation (line 93) | func NewClientInstallation(installationID int64) (*github.Client, error) {
  function setInstallationTransport (line 107) | func setInstallationTransport(installationID int64, itr ghinstallation.T...
  function getInstallationTransport (line 111) | func getInstallationTransport(installationID int64) *ghinstallation.Tran...
  function generateInstallationTransport (line 124) | func generateInstallationTransport(installationID int64) *ghinstallation...
  function CheckSignature (line 131) | func CheckSignature(installationID int64) error {
  function ExistRunnerReleases (line 140) | func ExistRunnerReleases(runnerVersion string) error {
  function ExistGitHubRepository (line 157) | func ExistGitHubRepository(scope string, accessToken string) error {
  function getRepositoryURL (line 184) | func getRepositoryURL(scope string) (string, error) {
  function getAPIEndpoint (line 208) | func getAPIEndpoint() (*url.URL, error) {

FILE: pkg/gh/github_test.go
  function TestDetectScope (line 10) | func TestDetectScope(t *testing.T) {
  type TestGetRepositoryURLInput (line 42) | type TestGetRepositoryURLInput struct
  function TestGetRepositoryURL (line 47) | func TestGetRepositoryURL(t *testing.T) {

FILE: pkg/gh/installation.go
  function listInstallations (line 12) | func listInstallations(ctx context.Context) ([]*github.Installation, err...
  function getCacheInstallationsKey (line 27) | func getCacheInstallationsKey() string {
  function _listInstallations (line 31) | func _listInstallations(ctx context.Context) ([]*github.Installation, er...
  function listAppsInstalledRepo (line 58) | func listAppsInstalledRepo(ctx context.Context, installationID int64) ([...
  function getCacheInstalledRepoKey (line 73) | func getCacheInstalledRepoKey(installationID int64) string {
  function _listAppsInstalledRepo (line 77) | func _listAppsInstalledRepo(ctx context.Context, installationID int64) (...
  function GetInstallationByID (line 106) | func GetInstallationByID(ctx context.Context, installationID int64) (*gi...
  function PurgeInstallationCache (line 122) | func PurgeInstallationCache(ctx context.Context) error {

FILE: pkg/gh/jwt.go
  function GenerateGitHubAppsToken (line 22) | func GenerateGitHubAppsToken(ctx context.Context, clientApps *github.Cli...
  function IsInstalledGitHubApp (line 34) | func IsInstalledGitHubApp(ctx context.Context, inputScope string) (int64...
  type ErrIsNotInstalledGitHubApps (line 72) | type ErrIsNotInstalledGitHubApps struct
    method Error (line 77) | func (e *ErrIsNotInstalledGitHubApps) Error() string {
    method Unwrap (line 81) | func (e *ErrIsNotInstalledGitHubApps) Unwrap() error {
  function isInstalledGitHubAppSelected (line 85) | func isInstalledGitHubAppSelected(ctx context.Context, inputScope string...

FILE: pkg/gh/jwt_test.go
  function setStubFunctions (line 11) | func setStubFunctions() {
  function Test_IsInstalledGitHubApp (line 62) | func Test_IsInstalledGitHubApp(t *testing.T) {

FILE: pkg/gh/label.go
  function IsRequestedMyshoesLabel (line 6) | func IsRequestedMyshoesLabel(labels []string) bool {

FILE: pkg/gh/metrics.go
  constant githubAPINamespace (line 16) | githubAPINamespace = "myshoes"
  type instrumentedTransport (line 67) | type instrumentedTransport struct
    method RoundTrip (line 81) | func (t *instrumentedTransport) RoundTrip(req *http.Request) (*http.Re...
  function newInstrumentedTransport (line 71) | func newInstrumentedTransport(next http.RoundTripper) http.RoundTripper {
  function classifyGitHubAPIError (line 121) | func classifyGitHubAPIError(err error) string {

FILE: pkg/gh/metrics_test.go
  type stubTransport (line 13) | type stubTransport struct
    method RoundTrip (line 18) | func (s *stubTransport) RoundTrip(req *http.Request) (*http.Response, ...
  type timeoutErr (line 25) | type timeoutErr struct
    method Error (line 27) | func (timeoutErr) Error() string   { return "timeout" }
    method Timeout (line 28) | func (timeoutErr) Timeout() bool   { return true }
    method Temporary (line 29) | func (timeoutErr) Temporary() bool { return true }
  function TestInstrumentedTransportMetrics (line 31) | func TestInstrumentedTransportMetrics(t *testing.T) {
  function TestInstrumentedTransportCacheHit (line 66) | func TestInstrumentedTransportCacheHit(t *testing.T) {
  function TestInstrumentedTransportErrorMetrics (line 94) | func TestInstrumentedTransportErrorMetrics(t *testing.T) {

FILE: pkg/gh/ratelimit.go
  function storeRateLimit (line 10) | func storeRateLimit(scope string, rateLimit github.Rate) {
  function getRateLimitKey (line 20) | func getRateLimitKey(org, repo string) string {
  function GetRateLimitRemain (line 29) | func GetRateLimitRemain() map[string]int {
  function GetRateLimitLimit (line 54) | func GetRateLimitLimit() map[string]int {

FILE: pkg/gh/runner.go
  function ExistGitHubRunner (line 14) | func ExistGitHubRunner(ctx context.Context, client *github.Client, owner...
  function ExistGitHubRunnerWithRunner (line 24) | func ExistGitHubRunnerWithRunner(runners []*github.Runner, runnerName st...
  function ListRunners (line 35) | func ListRunners(ctx context.Context, client *github.Client, owner, repo...
  function getCacheKey (line 69) | func getCacheKey(owner, repo string) string {
  function listRunners (line 73) | func listRunners(ctx context.Context, client *github.Client, owner, repo...
  function GetLatestRunnerVersion (line 90) | func GetLatestRunnerVersion(ctx context.Context, scope string) (string, ...
  function getRunnerVersion (line 128) | func getRunnerVersion(applications []*github.RunnerApplicationDownload) ...
  function ConcatLabels (line 142) | func ConcatLabels(checkEventJSON string) (string, error) {

FILE: pkg/gh/scope.go
  type Scope (line 6) | type Scope
    method String (line 16) | func (s Scope) String() string {
  constant Unknown (line 10) | Unknown Scope = iota
  constant Repository (line 11) | Repository
  constant Organization (line 12) | Organization
  function DetectScope (line 28) | func DetectScope(scope string) Scope {
  function DivideScope (line 41) | func DivideScope(scope string) (string, string) {

FILE: pkg/gh/token_registration.go
  function GetRunnerRegistrationToken (line 17) | func GetRunnerRegistrationToken(ctx context.Context, installationID int6...
  function generateRunnerRegisterToken (line 33) | func generateRunnerRegisterToken(ctx context.Context, installationID int...
  function setRunnerRegisterTokenCache (line 58) | func setRunnerRegisterTokenCache(installationID int64, scope, token stri...
  function getRunnerRegisterTokenFromCache (line 64) | func getRunnerRegisterTokenFromCache(installationID int64, scope string)...
  function getCacheKeyRegistrationToken (line 76) | func getCacheKeyRegistrationToken(installationID int64, scope string) st...

FILE: pkg/gh/webhook.go
  function parseEventJSON (line 12) | func parseEventJSON(in []byte) (interface{}, error) {
  function ExtractRunsOnLabels (line 35) | func ExtractRunsOnLabels(in []byte) ([]string, error) {

FILE: pkg/gh/workflow_job.go
  function listWorkflowJob (line 11) | func listWorkflowJob(ctx context.Context, client *github.Client, owner, ...
  function ListWorkflowJobByRunID (line 20) | func ListWorkflowJobByRunID(ctx context.Context, client *github.Client, ...
  function getWorkflowJobCacheKey (line 36) | func getWorkflowJobCacheKey(owner, repo string, runID int64) string {

FILE: pkg/gh/workflow_run.go
  function listWorkflowRuns (line 12) | func listWorkflowRuns(ctx context.Context, client *github.Client, owner,...
  function ListWorkflowRunsNewest (line 21) | func ListWorkflowRunsNewest(ctx context.Context, client *github.Client, ...
  function getRunsCacheKey (line 57) | func getRunsCacheKey(owner, repo string) string {

FILE: pkg/logger/logger.go
  function SetLogger (line 17) | func SetLogger(l *log.Logger) {
  function Logf (line 27) | func Logf(isDebug bool, format string, v ...interface{}) {

FILE: pkg/metric/collector.go
  constant namespace (line 15) | namespace = "myshoes"
  type Collector (line 27) | type Collector struct
    method Describe (line 45) | func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
    method Collect (line 52) | func (c *Collector) Collect(ch chan<- prometheus.Metric) {
    method scrape (line 60) | func (c *Collector) scrape(ctx context.Context, ch chan<- prometheus.M...
  function NewCollector (line 35) | func NewCollector(ctx context.Context, ds datastore.Datastore) *Collector {
  type Scraper (line 83) | type Scraper interface
  function NewScrapers (line 90) | func NewScrapers() []Scraper {
  type Metrics (line 99) | type Metrics struct
  function NewMetrics (line 106) | func NewMetrics() Metrics {

FILE: pkg/metric/scrape_datastore.go
  constant datastoreName (line 18) | datastoreName = "datastore"
  type ScraperDatastore (line 63) | type ScraperDatastore struct
    method Name (line 66) | func (ScraperDatastore) Name() string {
    method Help (line 71) | func (ScraperDatastore) Help() string {
    method Scrape (line 76) | func (ScraperDatastore) Scrape(ctx context.Context, ds datastore.Datas...
  function scrapeJobs (line 90) | func scrapeJobs(ctx context.Context, ds datastore.Datastore, ch chan<- p...
  function scrapeJobCounter (line 169) | func scrapeJobCounter(ctx context.Context, ds datastore.Datastore, ch ch...
  function scrapeTargets (line 181) | func scrapeTargets(ctx context.Context, ds datastore.Datastore, ch chan<...
  function scrapeRunners (line 212) | func scrapeRunners(ctx context.Context, ds datastore.Datastore, ch chan<...

FILE: pkg/metric/scrape_github.go
  constant githubName (line 16) | githubName = "github"
  type ScraperGitHub (line 45) | type ScraperGitHub struct
    method Name (line 48) | func (ScraperGitHub) Name() string {
    method Help (line 53) | func (ScraperGitHub) Help() string {
    method Scrape (line 58) | func (s ScraperGitHub) Scrape(ctx context.Context, ds datastore.Datast...
  function scrapePendingRuns (line 68) | func scrapePendingRuns(ctx context.Context, ds datastore.Datastore, ch c...
  function scrapeInstallation (line 107) | func scrapeInstallation(ctx context.Context, ch chan<- prometheus.Metric...

FILE: pkg/metric/scrape_memory.go
  constant memoryName (line 19) | memoryName = "memory"
  type ScraperMemory (line 85) | type ScraperMemory struct
    method Name (line 88) | func (ScraperMemory) Name() string {
    method Help (line 93) | func (ScraperMemory) Help() string {
    method Scrape (line 98) | func (ScraperMemory) Scrape(ctx context.Context, ds datastore.Datastor...
  function scrapeStarterValues (line 113) | func scrapeStarterValues(ch chan<- prometheus.Metric) error {
  function scrapeGitHubValues (line 162) | func scrapeGitHubValues(ch chan<- prometheus.Metric) error {
  function scrapeDockerValues (line 182) | func scrapeDockerValues(ch chan<- prometheus.Metric) error {

FILE: pkg/runner/runner.go
  type Manager (line 28) | type Manager struct
    method Loop (line 42) | func (m *Manager) Loop(ctx context.Context) error {
  function New (line 34) | func New(ds datastore.Datastore, runnerVersion string) *Manager {
  type TemporaryMode (line 81) | type TemporaryMode
    method StringFlag (line 91) | func (rtm TemporaryMode) StringFlag() string {
  constant TemporaryUnknown (line 85) | TemporaryUnknown TemporaryMode = iota
  constant TemporaryOnce (line 86) | TemporaryOnce
  constant TemporaryEphemeral (line 87) | TemporaryEphemeral
  function GetRunnerTemporaryMode (line 102) | func GetRunnerTemporaryMode(runnerVersion string) (string, TemporaryMode...

FILE: pkg/runner/runner_delete.go
  method do (line 36) | func (m *Manager) do(ctx context.Context) error {
  method removeRunners (line 55) | func (m *Manager) removeRunners(ctx context.Context, t datastore.Target)...
  method removeRunner (line 151) | func (m *Manager) removeRunner(ctx context.Context, t datastore.Target, ...
  function isRegisteredRunnerZeroInGitHub (line 181) | func isRegisteredRunnerZeroInGitHub(ctx context.Context, t datastore.Tar...
  constant ErrDescriptionRunnerForQueueingIsNotFound (line 203) | ErrDescriptionRunnerForQueueingIsNotFound = "runner for queueing is not ...
  function sanitizeGitHubRunner (line 213) | func sanitizeGitHubRunner(ghRunner github.Runner, dsRunner datastore.Run...
  function sanitizeRunnerMustRunningTime (line 237) | func sanitizeRunnerMustRunningTime(runner datastore.Runner) error {
  function sanitizeRunner (line 241) | func sanitizeRunner(runner datastore.Runner, needTime time.Duration) err...
  method deleteRunnerWithGitHub (line 253) | func (m *Manager) deleteRunnerWithGitHub(ctx context.Context, githubClie...
  method deleteRunner (line 277) | func (m *Manager) deleteRunner(ctx context.Context, runner datastore.Run...

FILE: pkg/runner/runner_delete_ephemeral.go
  method removeRunnerModeEphemeral (line 16) | func (m *Manager) removeRunnerModeEphemeral(ctx context.Context, t datas...

FILE: pkg/runner/runner_delete_once.go
  method removeRunnerModeOnce (line 16) | func (m *Manager) removeRunnerModeOnce(ctx context.Context, t datastore....

FILE: pkg/runner/token_update.go
  method doTargetToken (line 13) | func (m *Manager) doTargetToken(ctx context.Context) error {

FILE: pkg/runner/util.go
  function ToName (line 12) | func ToName(u string) string {
  function ToUUID (line 17) | func ToUUID(name string) (uuid.UUID, error) {
  function ToReason (line 23) | func ToReason(status string) datastore.RunnerStatus {

FILE: pkg/shoes/shoes.go
  function GetClient (line 21) | func GetClient() (Client, func(), error) {
  type Plugin (line 56) | type Plugin struct
    method GRPCServer (line 63) | func (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server)...
    method GRPCClient (line 68) | func (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBr...
  type Client (line 73) | type Client interface
  type GRPCClient (line 79) | type GRPCClient struct
    method AddInstance (line 84) | func (c *GRPCClient) AddInstance(ctx context.Context, runnerName, setu...
    method DeleteInstance (line 104) | func (c *GRPCClient) DeleteInstance(ctx context.Context, cloudID strin...

FILE: pkg/starter/error.go
  type Error (line 5) | type Error struct
    method Error (line 10) | func (e Error) Error() string {
    method Unwrap (line 14) | func (e Error) Unwrap() error {
    method Is (line 43) | func (e Error) Is(target error) bool {
  type internalError (line 18) | type internalError
    method String (line 24) | func (i internalError) String() string {
  constant errorInvalidLabel (line 21) | errorInvalidLabel internalError = iota
  function NewInvalidLabel (line 37) | func NewInvalidLabel(err error) error {

FILE: pkg/starter/metric.go
  function incrementDeleteJobMap (line 16) | func incrementDeleteJobMap(j datastore.Job) error {

FILE: pkg/starter/safety/safety.go
  type Safety (line 8) | type Safety interface

FILE: pkg/starter/safety/unlimited/unlimited.go
  type Unlimited (line 7) | type Unlimited struct
    method Check (line 10) | func (u Unlimited) Check(job *datastore.Job) (bool, error) {

FILE: pkg/starter/scripts.go
  function getPatchedFiles (line 21) | func getPatchedFiles() (string, error) {
  type templateCompressedScriptValue (line 25) | type templateCompressedScriptValue struct
  method GetSetupScript (line 30) | func (s *Starter) GetSetupScript(ctx context.Context, targetScope, runne...
  method getSetupRawScript (line 65) | func (s *Starter) getSetupRawScript(ctx context.Context, targetScope, ru...
  function labelsToOneLine (line 124) | func labelsToOneLine(labels []string) string {
  constant templateCompressedScript (line 132) | templateCompressedScript = `#!/bin/bash
  type templateCreateLatestRunnerOnceValue (line 145) | type templateCreateLatestRunnerOnceValue struct
  constant templateCreateLatestRunnerOnce (line 161) | templateCreateLatestRunnerOnce = `#!/bin/bash

FILE: pkg/starter/starter.go
  type Starter (line 48) | type Starter struct
    method Loop (line 66) | func (s *Starter) Loop(ctx context.Context) error {
    method dispatcher (line 119) | func (s *Starter) dispatcher(ctx context.Context, ch chan datastore.Jo...
    method run (line 134) | func (s *Starter) run(ctx context.Context, ch chan datastore.Job) error {
    method ProcessJob (line 212) | func (s *Starter) ProcessJob(ctx context.Context, job datastore.Job) e...
    method bung (line 336) | func (s *Starter) bung(ctx context.Context, job datastore.Job, target ...
    method checkRegisteredRunner (line 409) | func (s *Starter) checkRegisteredRunner(ctx context.Context, runnerNam...
    method reRunWorkflow (line 442) | func (s *Starter) reRunWorkflow(ctx context.Context) {
  function New (line 56) | func New(ds datastore.Datastore, s safety.Safety, runnerVersion string, ...
  function extractWorkflowIDs (line 193) | func extractWorkflowIDs(job datastore.Job) (runID int64, jobID int64, er...
  function getTargetScope (line 381) | func getTargetScope(target datastore.Target, job datastore.Job) string {
  function deleteInstance (line 388) | func deleteInstance(ctx context.Context, cloudID, checkEventJSON string)...
  function reRunWorkflowByPendingRun (line 457) | func reRunWorkflowByPendingRun(ctx context.Context, ds datastore.Datasto...
  function enqueueRescueRun (line 464) | func enqueueRescueRun(ctx context.Context, pendingRun datastore.PendingW...
  function enqueueRescueJob (line 538) | func enqueueRescueJob(ctx context.Context, workflowJob *github.WorkflowJ...

FILE: pkg/web/config.go
  type inputConfigDebug (line 11) | type inputConfigDebug struct
  type inputConfigStrict (line 15) | type inputConfigStrict struct
  function handleConfigDebug (line 19) | func handleConfigDebug(w http.ResponseWriter, r *http.Request) {
  function handleConfigStrict (line 33) | func handleConfigStrict(w http.ResponseWriter, r *http.Request) {

FILE: pkg/web/http.go
  function NewMux (line 19) | func NewMux(ds datastore.Datastore) *goji.Mux {
  function Serve (line 82) | func Serve(ctx context.Context, ds datastore.Datastore) error {
  function apacheLogging (line 107) | func apacheLogging(r *http.Request) {

FILE: pkg/web/http_test.go
  function TestMain (line 10) | func TestMain(m *testing.M) {

FILE: pkg/web/metrics.go
  function HandleMetrics (line 15) | func HandleMetrics(w http.ResponseWriter, r *http.Request, ds datastore....

FILE: pkg/web/target.go
  type TargetCreateParam (line 23) | type TargetCreateParam struct
    method ToDS (line 314) | func (t *TargetCreateParam) ToDS(appToken string, tokenExpired time.Ti...
  type UserTarget (line 32) | type UserTarget struct
  function sortUserTarget (line 44) | func sortUserTarget(uts []UserTarget) []UserTarget {
  function handleTargetList (line 70) | func handleTargetList(w http.ResponseWriter, r *http.Request, ds datasto...
  function handleTargetRead (line 93) | func handleTargetRead(w http.ResponseWriter, r *http.Request, ds datasto...
  function sanitizeTarget (line 116) | func sanitizeTarget(t datastore.Target) UserTarget {
  function handleTargetUpdate (line 132) | func handleTargetUpdate(w http.ResponseWriter, r *http.Request, ds datas...
  function handleTargetDelete (line 187) | func handleTargetDelete(w http.ResponseWriter, r *http.Request, ds datas...
  function parseReqTargetID (line 222) | func parseReqTargetID(r *http.Request) (uuid.UUID, error) {
  type ErrorResponse (line 233) | type ErrorResponse struct
  function outputErrorMsg (line 237) | func outputErrorMsg(w http.ResponseWriter, status int, msg string) {
  function validateUpdateTarget (line 246) | func validateUpdateTarget(old, new datastore.Target) error {
  function isValidTargetCreateParam (line 292) | func isValidTargetCreateParam(input TargetCreateParam) error {
  function toNullString (line 300) | func toNullString(input *string) sql.NullString {
  type getWillUpdateTargetVariableOld (line 327) | type getWillUpdateTargetVariableOld struct
  type getWillUpdateTargetVariableNew (line 332) | type getWillUpdateTargetVariableNew struct
  function getWillUpdateTargetVariable (line 337) | func getWillUpdateTargetVariable(oldParam getWillUpdateTargetVariableOld...
  function getWillUpdateTargetVariableString (line 348) | func getWillUpdateTargetVariableString(old sql.NullString, new *string) ...

FILE: pkg/web/target_create.go
  function handleTargetCreate (line 20) | func handleTargetCreate(w http.ResponseWriter, r *http.Request, ds datas...
  function isValidScopeAndToken (line 126) | func isValidScopeAndToken(ctx context.Context, scope, githubPersonalToke...
  function createNewTarget (line 146) | func createNewTarget(ctx context.Context, input datastore.Target, ds dat...

FILE: pkg/web/target_test.go
  function parseResponse (line 27) | func parseResponse(resp *http.Response) ([]byte, int) {
  function setStubFunctions (line 37) | func setStubFunctions() {
  function Test_handleTargetCreate (line 67) | func Test_handleTargetCreate(t *testing.T) {
  function Test_handleTargetCreate_alreadyRegistered (line 139) | func Test_handleTargetCreate_alreadyRegistered(t *testing.T) {
  function Test_handleTargetCreate_recreated (line 169) | func Test_handleTargetCreate_recreated(t *testing.T) {
  function Test_handleTargetCreate_recreated_update (line 228) | func Test_handleTargetCreate_recreated_update(t *testing.T) {
  function Test_handleTargetList (line 289) | func Test_handleTargetList(t *testing.T) {
  function Test_handleTargetRead (line 359) | func Test_handleTargetRead(t *testing.T) {
  function Test_handleTargetUpdate (line 423) | func Test_handleTargetUpdate(t *testing.T) {
  function Test_handleTargetUpdate_Error (line 534) | func Test_handleTargetUpdate_Error(t *testing.T) {
  function Test_handleTargetDelete (line 586) | func Test_handleTargetDelete(t *testing.T) {

FILE: pkg/web/webhook.go
  function HandleGitHubEvent (line 24) | func HandleGitHubEvent(w http.ResponseWriter, r *http.Request, ds datast...
  function receivePingWebhook (line 108) | func receivePingWebhook(_ context.Context, event *github.PingEvent) error {
  function receiveCheckRunWebhook (line 113) | func receiveCheckRunWebhook(ctx context.Context, event *github.CheckRunE...
  function processCheckRun (line 143) | func processCheckRun(ctx context.Context, ds datastore.Datastore, repoNa...
  function receiveWorkflowJobWebhook (line 188) | func receiveWorkflowJobWebhook(ctx context.Context, event *github.Workfl...
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (394K chars).
[
  {
    "path": ".github/workflows/build-docker-sha.yaml",
    "chars": 1427,
    "preview": "name: Build Docker image (sha)\non:\n  push:\n    branches:\n      - \"**\"\n  workflow_dispatch:\n\njobs:\n  docker-build-sha:\n  "
  },
  {
    "path": ".github/workflows/release.yaml",
    "chars": 1773,
    "preview": "name: release\non:\n  push:\n    tags:\n      - \"v[0-9]+.[0-9]+.[0-9]+\"\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    "
  },
  {
    "path": ".github/workflows/test.yaml",
    "chars": 1166,
    "preview": "name: test\non:\n  push:\n    branches:\n      - \"**\"\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: ${{ m"
  },
  {
    "path": ".gitignore",
    "chars": 3284,
    "preview": "\n# Created by https://www.toptal.com/developers/gitignore/api/macos,intellij,go\n# Edit at https://www.toptal.com/develop"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 101,
    "preview": "builds:\n  - main: ./cmd/server/cmd.go\n    goos:\n      - linux\n    goarch:\n      - amd64\n      - arm64"
  },
  {
    "path": "Dockerfile",
    "chars": 592,
    "preview": "FROM golang:1.25 AS builder\n\nWORKDIR /go/src/github.com/whywaita/myshoes\n\nRUN go install google.golang.org/protobuf/cmd/"
  },
  {
    "path": "LICENSE",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2021 whywaita\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "Makefile",
    "chars": 872,
    "preview": ".PHONY: help\n.DEFAULT_GOAL := help\n\nCURRENT_REVISION = $(shell git rev-parse --short HEAD)\nBUILD_LDFLAGS = \"-X main.revi"
  },
  {
    "path": "README.md",
    "chars": 2591,
    "preview": "# myshoes: Auto scaling self-hosted runner for GitHub Actions\n\n![](./docs/assets/img/myshoes_logo_yoko_colorA.png)\n\n[![a"
  },
  {
    "path": "api/myshoes/README.md",
    "chars": 577,
    "preview": "# myshoes-sdk-go\n\nThe Go SDK for myshoes\n\n## Usage\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/ht"
  },
  {
    "path": "api/myshoes/client.go",
    "chars": 1570,
    "preview": "package myshoes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n)\n\n// Client is a cli"
  },
  {
    "path": "api/myshoes/http.go",
    "chars": 1130,
    "preview": "package myshoes\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/web\"\n)\n\nfunc deco"
  },
  {
    "path": "api/myshoes/target.go",
    "chars": 2573,
    "preview": "package myshoes\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/web"
  },
  {
    "path": "api/proto/myshoes.proto",
    "chars": 853,
    "preview": "syntax = \"proto3\";\n\npackage whywaita.myshoes;\noption go_package = \"github.com/whywaita/myshoes/api/proto.go\";\n\nservice S"
  },
  {
    "path": "api/proto.go/myshoes.pb.go",
    "chars": 12719,
    "preview": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        v5.29.3\n// so"
  },
  {
    "path": "api/proto.go/myshoes_grpc.pb.go",
    "chars": 5890,
    "preview": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc           "
  },
  {
    "path": "cmd/server/cmd.go",
    "chars": 2905,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes"
  },
  {
    "path": "cmd/shoes-tester/main.go",
    "chars": 9089,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strc"
  },
  {
    "path": "docs/01_01_for_admin_setup.md",
    "chars": 4606,
    "preview": "# Setup myshoes daemon\n\n## Goal\n\n- Start myshoes daemon\n\n## Prepare\n\n- The network connectivity to myshoes server.\n  - T"
  },
  {
    "path": "docs/01_02_for_admin_tips.md",
    "chars": 348,
    "preview": "# Tips for myshoes admin\n\n## Job management hooks for self-hosted runners\n\nYou can use job management hooks for self-hos"
  },
  {
    "path": "docs/02_01_for_user_setup.md",
    "chars": 3479,
    "preview": "# Setup (only once)\n\n## Goal\n\n- Start provision runner\n\n## Prepare\n\n- Get GitHub Apps's Public page from myshoes admin.\n"
  },
  {
    "path": "docs/03_how-to-develop-shoes.md",
    "chars": 3041,
    "preview": "# How to develop shoes provider\n\n## TL;DR\n\n- implement to gRPC server\n    - `shoes`, `health`, `stdio`\n- define resource"
  },
  {
    "path": "docs/assets/myshoes.service",
    "chars": 275,
    "preview": "[Unit]\nDescription=myshoes is Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions\nAfter=net"
  },
  {
    "path": "go.mod",
    "chars": 3913,
    "preview": "module github.com/whywaita/myshoes\n\ngo 1.25\n\nrequire (\n\tgithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0\n\tgithub.com/g"
  },
  {
    "path": "go.sum",
    "chars": 23427,
    "preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
  },
  {
    "path": "internal/testutils/mysql.go",
    "chars": 1940,
    "preview": "package testutils\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"githu"
  },
  {
    "path": "internal/testutils/testutils.go",
    "chars": 1980,
    "preview": "package testutils\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n"
  },
  {
    "path": "internal/testutils/web.go",
    "chars": 174,
    "preview": "package testutils\n\n// GetTestURL return url of httptest.Server\nfunc GetTestURL() string {\n\tif testURL == \"\" {\n\t\tpanic(\"t"
  },
  {
    "path": "internal/util/util.go",
    "chars": 343,
    "preview": "package util\n\nimport (\n\t\"math/rand/v2\"\n\t\"time\"\n)\n\n// CalcRetryTime is caliculate retry time by exponential backoff and j"
  },
  {
    "path": "pkg/config/config.go",
    "chars": 3245,
    "preview": "package config\n\nimport (\n\t\"crypto/rsa\"\n\t\"strings\"\n)\n\n// Config is config value\nvar Config Conf\n\n// Conf is type of Confi"
  },
  {
    "path": "pkg/config/init.go",
    "chars": 8632,
    "preview": "package config\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\""
  },
  {
    "path": "pkg/datastore/github.go",
    "chars": 5871,
    "preview": "package datastore\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/whywaita/mysho"
  },
  {
    "path": "pkg/datastore/interface.go",
    "chars": 7072,
    "preview": "package datastore\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\tuuid \"github.com"
  },
  {
    "path": "pkg/datastore/memory/memory.go",
    "chars": 5674,
    "preview": "package memory\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github."
  },
  {
    "path": "pkg/datastore/mysql/job.go",
    "chars": 1470,
    "preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github.com/whyw"
  },
  {
    "path": "pkg/datastore/mysql/job_test.go",
    "chars": 5359,
    "preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/"
  },
  {
    "path": "pkg/datastore/mysql/lock.go",
    "chars": 1199,
    "preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t"
  },
  {
    "path": "pkg/datastore/mysql/mysql.go",
    "chars": 1073,
    "preview": "package mysql\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-sql-driver/mysql\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\n// MySQL is impl"
  },
  {
    "path": "pkg/datastore/mysql/mysql_test.go",
    "chars": 177,
    "preview": "package mysql_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n)\n\nfunc TestMain(m *tes"
  },
  {
    "path": "pkg/datastore/mysql/runner.go",
    "chars": 5122,
    "preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github."
  },
  {
    "path": "pkg/datastore/mysql/runner_test.go",
    "chars": 14592,
    "preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/"
  },
  {
    "path": "pkg/datastore/mysql/schema.sql",
    "chars": 2993,
    "preview": "CREATE TABLE `targets` (\n    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,\n    `scope` VARCHAR(255) NOT NULL,\n    `ghe_domain"
  },
  {
    "path": "pkg/datastore/mysql/target.go",
    "chars": 4040,
    "preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github."
  },
  {
    "path": "pkg/datastore/mysql/target_test.go",
    "chars": 20735,
    "preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/"
  },
  {
    "path": "pkg/datastore/resource_type.go",
    "chars": 4243,
    "preview": "package datastore\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\tpb \"github.com/whywaita/myshoes/api/proto.g"
  },
  {
    "path": "pkg/docker/ratelimit.go",
    "chars": 3135,
    "preview": "package docker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/golang-jw"
  },
  {
    "path": "pkg/gh/github.go",
    "chars": 7156,
    "preview": "package gh\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bradleyfalzon/ghinstallation/v2"
  },
  {
    "path": "pkg/gh/github_test.go",
    "chars": 2442,
    "preview": "package gh\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n)\n\nfunc TestDetectScope(t *testing.T) {"
  },
  {
    "path": "pkg/gh/installation.go",
    "chars": 3782,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/myshoes/"
  },
  {
    "path": "pkg/gh/jwt.go",
    "chars": 3274,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\n\t\"github.com/googl"
  },
  {
    "path": "pkg/gh/jwt_test.go",
    "chars": 2575,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\nfunc setStubFunctions()"
  },
  {
    "path": "pkg/gh/label.go",
    "chars": 443,
    "preview": "package gh\n\nimport \"strings\"\n\n// IsRequestedMyshoesLabel checks if the job has appropriate labels for myshoes\nfunc IsReq"
  },
  {
    "path": "pkg/gh/metrics.go",
    "chars": 3352,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/m4ns0ur/httpcache\"\n\t\"github.co"
  },
  {
    "path": "pkg/gh/metrics_test.go",
    "chars": 3864,
    "preview": "package gh\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/m4ns0ur/httpcache\"\n\t\"github.com/prometheus/clie"
  },
  {
    "path": "pkg/gh/ratelimit.go",
    "chars": 1280,
    "preview": "package gh\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\nfunc storeRateLimit(scope string, rate"
  },
  {
    "path": "pkg/gh/runner.go",
    "chars": 4905,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywai"
  },
  {
    "path": "pkg/gh/scope.go",
    "chars": 883,
    "preview": "package gh\n\nimport \"strings\"\n\n// Scope is scope for auto-scaling target\ntype Scope int\n\n// Scope values\nconst (\n\tUnknown"
  },
  {
    "path": "pkg/gh/token_registration.go",
    "chars": 2648,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/patrickmn/go-cache\"\n)\n\nvar (\n\tcacheRegistrationToken = cach"
  },
  {
    "path": "pkg/gh/webhook.go",
    "chars": 1297,
    "preview": "package gh\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\n// parseEventJSON parse a jso"
  },
  {
    "path": "pkg/gh/workflow_job.go",
    "chars": 1316,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n)\n\nfunc listWorkflowJob(ctx co"
  },
  {
    "path": "pkg/gh/workflow_run.go",
    "chars": 1812,
    "preview": "package gh\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/myshoes/"
  },
  {
    "path": "pkg/logger/logger.go",
    "chars": 680,
    "preview": "package logger\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n)\n\nvar (\n\tlogger = log.New(os.S"
  },
  {
    "path": "pkg/metric/collector.go",
    "chars": 3224,
    "preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.c"
  },
  {
    "path": "pkg/metric/scrape_datastore.go",
    "chars": 6034,
    "preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\""
  },
  {
    "path": "pkg/metric/scrape_github.go",
    "chars": 3644,
    "preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\n\t\"gith"
  },
  {
    "path": "pkg/metric/scrape_memory.go",
    "chars": 6462,
    "preview": "package metric\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/prometheus/cl"
  },
  {
    "path": "pkg/metric/webhook.go",
    "chars": 1229,
    "preview": "package metric\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometh"
  },
  {
    "path": "pkg/runner/metrics.go",
    "chars": 976,
    "preview": "package runner\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometh"
  },
  {
    "path": "pkg/runner/runner.go",
    "chars": 2991,
    "preview": "package runner\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/whywaita/myshoes/pkg"
  },
  {
    "path": "pkg/runner/runner_delete.go",
    "chars": 9780,
    "preview": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/google/go-g"
  },
  {
    "path": "pkg/runner/runner_delete_ephemeral.go",
    "chars": 1974,
    "preview": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/my"
  },
  {
    "path": "pkg/runner/runner_delete_once.go",
    "chars": 2038,
    "preview": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/my"
  },
  {
    "path": "pkg/runner/token_update.go",
    "chars": 1689,
    "preview": "package runner\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/m"
  },
  {
    "path": "pkg/runner/util.go",
    "chars": 722,
    "preview": "package runner\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github.com/whywaita/myshoes/pkg/datastor"
  },
  {
    "path": "pkg/shoes/shoes.go",
    "chars": 3338,
    "preview": "package shoes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/hashicorp/go-plugin\"\n\n\tpb \"github.com/whywaita/"
  },
  {
    "path": "pkg/starter/README.md",
    "chars": 51,
    "preview": "## starter\n\nstarter is a dispatcher for running job"
  },
  {
    "path": "pkg/starter/error.go",
    "chars": 732,
    "preview": "package starter\n\nimport \"errors\"\n\ntype Error struct {\n\tkind internalError\n\terr  error\n}\n\nfunc (e Error) Error() string {"
  },
  {
    "path": "pkg/starter/metric.go",
    "chars": 606,
    "preview": "package starter\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg"
  },
  {
    "path": "pkg/starter/metrics.go",
    "chars": 967,
    "preview": "package starter\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/promet"
  },
  {
    "path": "pkg/starter/safety/README.md",
    "chars": 62,
    "preview": "# safety\n\nsafety is interface of check to enable runner start."
  },
  {
    "path": "pkg/starter/safety/safety.go",
    "chars": 253,
    "preview": "package safety\n\nimport (\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// Safety is interface for safety\ntype Safety i"
  },
  {
    "path": "pkg/starter/safety/unlimited/unlimited.go",
    "chars": 295,
    "preview": "package unlimited\n\nimport \"github.com/whywaita/myshoes/pkg/datastore\"\n\n// Unlimited is implement of safety.\n// Unlimited"
  },
  {
    "path": "pkg/starter/scripts/RunnerService.js",
    "chars": 3151,
    "preview": "#!/usr/bin/env node\n// Copyright (c) GitHub. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in"
  },
  {
    "path": "pkg/starter/scripts.go",
    "chars": 11802,
    "preview": "package starter\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t_ \"embed\" // TODO:\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strings\"\n"
  },
  {
    "path": "pkg/starter/starter.go",
    "chars": 18947,
    "preview": "package starter\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sync\"\n\t\"sync/atomic\""
  },
  {
    "path": "pkg/web/config.go",
    "chars": 1107,
    "preview": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/mysh"
  },
  {
    "path": "pkg/web/http.go",
    "chars": 2921,
    "preview": "package web\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n"
  },
  {
    "path": "pkg/web/http_test.go",
    "chars": 175,
    "preview": "package web_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n)\n\nfunc TestMain(m *testi"
  },
  {
    "path": "pkg/web/metrics.go",
    "chars": 638,
    "preview": "package web\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\n\t\"github.com/whywaita/myshoes/pkg/metri"
  },
  {
    "path": "pkg/web/target.go",
    "chars": 10208,
    "preview": "package web\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/r3lab"
  },
  {
    "path": "pkg/web/target_create.go",
    "chars": 5712,
    "preview": "package web\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\tuuid \"github.co"
  },
  {
    "path": "pkg/web/target_test.go",
    "chars": 19764,
    "preview": "package web_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n"
  },
  {
    "path": "pkg/web/webhook.go",
    "chars": 7028,
    "preview": "package web\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gi"
  }
]

About this extraction

This page contains the full source code of the whywaita/myshoes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (350.2 KB), approximately 106.4k tokens, and a symbol index with 505 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!