[
  {
    "path": ".github/workflows/build-docker-sha.yaml",
    "content": "name: Build Docker image (sha)\non:\n  push:\n    branches:\n      - \"**\"\n  workflow_dispatch:\n\njobs:\n  docker-build-sha:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n      - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0\n      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1\n      - name: Cache Docker layers\n        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0\n        id: meta\n        with:\n          images: ghcr.io/${{ github.repository_owner }}/myshoes\n          tags: |\n            type=sha\n      - name: Build container image\n        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0\n        with:\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "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    steps:\n      - name: Checkout\n        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n        with:\n          fetch-depth: 0\n      - name: Setup Go\n        uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0\n        with:\n          go-version-file: 'go.mod'\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0\n        with:\n          version: latest\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n      - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0\n      - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.repository_owner }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0\n        id: meta\n        with:\n          images: ghcr.io/whywaita/myshoes\n          tags: |\n            type=raw,value=latest\n            type=semver,pattern={{raw}}\n            type=sha\n      - name: Build container image\n        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0\n        with:\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: test\non:\n  push:\n    branches:\n      - \"**\"\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        os:\n          - ubuntu-latest\n    steps:\n      - name: checkout\n        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n        with:\n          fetch-depth: 1\n      - name: setup go\n        uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0\n        with:\n          go-version-file: 'go.mod'\n      - name: lint\n        run: |\n          go install honnef.co/go/tools/cmd/staticcheck@latest\n          staticcheck ./...\n      - name: vet\n        run: |\n          go vet ./...\n      - name: test\n        run: |\n          make test\n  docker-build-test:\n    runs-on: ubuntu-latest\n    steps:\n     - name: checkout\n       uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n       with:\n         fetch-depth: 1\n     - name: Build container image\n       uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0\n       with:\n         push: false\n         tags: ${{ steps.meta.outputs.tags }}\n"
  },
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.toptal.com/developers/gitignore/api/macos,intellij,go\n# Edit at https://www.toptal.com/developers/gitignore?templates=macos,intellij,go\n\n### Go ###\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n### Go Patch ###\n/vendor/\n/Godeps/\n\n### Intellij ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# End of https://www.toptal.com/developers/gitignore/api/macos,intellij,go\n\n/myshoes*"
  },
  {
    "path": ".goreleaser.yml",
    "content": "builds:\n  - main: ./cmd/server/cmd.go\n    goos:\n      - linux\n    goarch:\n      - amd64\n      - arm64"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.25 AS builder\n\nWORKDIR /go/src/github.com/whywaita/myshoes\n\nRUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2\nRUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1\nRUN apt-get update -y \\\n    && apt-get install -y protobuf-compiler\n\nENV CGO_ENABLED=0\nENV GOOS=linux\nENV GOARCH=amd64\n\nCOPY . .\nRUN make build-linux\n\nFROM alpine\n\nRUN apk update \\\n  && apk update\nRUN apk add --no-cache ca-certificates \\\n  && update-ca-certificates 2>/dev/null || true\n\nCOPY --from=builder /go/src/github.com/whywaita/myshoes/myshoes-linux-amd64 /app\n\nCMD [\"/app\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 whywaita\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Makefile",
    "content": ".PHONY: help\n.DEFAULT_GOAL := help\n\nCURRENT_REVISION = $(shell git rev-parse --short HEAD)\nBUILD_LDFLAGS = \"-X main.revision=$(CURRENT_REVISION)\"\n\nhelp:\n\t@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":.*?## \"}; {printf \"\\033[36m%-20s\\033[0m %s\\n\", $$1, $$2}'\n\nbuild: ## Build All\n\tgo generate ./...\n\tmake build-proto\n\tgo build -o myshoes -ldflags $(BUILD_LDFLAGS) cmd/server/cmd.go\n\nbuild-linux: ## Build for Linux\n\tgo generate ./...\n\tmake build-proto\n\tGOOS=linux GOARCH=amd64 go build -o myshoes-linux-amd64 -ldflags $(BUILD_LDFLAGS) cmd/server/cmd.go\n\nbuild-proto: ## Build proto file\n\tmkdir -p tmp/proto-go\n\trm -rf api/proto.go\n\n\tprotoc -I=api/proto/ --go_out=tmp/proto-go/ --go-grpc_out=tmp/proto-go/ api/proto/**.proto\n\tmv tmp/proto-go/github.com/whywaita/myshoes/api/proto.go api/\n\trm -rf tmp\n\ntest: ## Exec test\n\tgo test -v ./..."
  },
  {
    "path": "README.md",
    "content": "# myshoes: Auto scaling self-hosted runner for GitHub Actions\n\n![](./docs/assets/img/myshoes_logo_yoko_colorA.png)\n\n[![awesome-runners](https://img.shields.io/badge/listed%20on-awesome--runners-blue.svg)](https://github.com/jonico/awesome-runners)\n[![Go Reference](https://pkg.go.dev/badge/github.com/whywaita/myshoes.svg)](https://pkg.go.dev/github.com/whywaita/myshoes)\n[![test](https://github.com/whywaita/myshoes/actions/workflows/test.yaml/badge.svg)](https://github.com/whywaita/myshoes/actions/workflows/test.yaml)\n[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)\n[![Go Report Card](https://goreportcard.com/badge/github.com/whywaita/myshoes)](https://goreportcard.com/report/github.com/whywaita/myshoes)\n\nAuto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions!\n\n## Features\n\n- Auto-scaling and runner with your cloud-provider\n    - your infrastructure (private cloud, homelab...)\n        - [LXD](https://linuxcontainers.org): [shoes-lxd](https://github.com/whywaita/myshoes-providers/tree/master/shoes-lxd)\n        - [OpenStack](https://www.openstack.org): [shoes-openstack](https://github.com/whywaita/myshoes-providers/tree/master/shoes-openstack)\n    - a low-cost instance in public cloud\n        - [AWS EC2 Spot Instances](https://aws.amazon.com/ec2/spot): [shoes-aws](https://github.com/whywaita/myshoes-providers/tree/master/shoes-aws)\n        - [GCP Preemptible VM instances](https://cloud.google.com/compute/docs/instances/preemptible): shoes-gcp (not yet)\n    - using special hardware\n        - Graphics Processing Unit (GPU)\n        - Field Programmable Gate Array (FPGA)\n    - And more in [whywaita/myshoes-providers](https://github.com/whywaita/myshoes-providers)\n\n## Setup (only once)\n\nPlease see [Documents](./docs).\n\n## How to contribute\n\n1. Fork it\n1. Clone original repository `git clone https://github.com/whywaita/myshoes`\n1. Add remote your repository `git remote add your-name https://github.com/${your-name}/myshoes`\n1. Create your feature branch `git switch -c my-new-feature`\n1. Commit your changes `git commit -am 'Add some feature'`\n1. Push to the branch `git push your-name my-new-feature`\n1. Create new Pull Request\n\n## Publications\n\n### Talk\n\n- [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)\n- [Development OSS CI/CD platform in CyberAgent (ja)](https://www.slideshare.net/whywaita/cyberagent-oss-cicd-myshoes-cicd2021)\n"
  },
  {
    "path": "api/myshoes/README.md",
    "content": "# 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/http\"\n\t\n\t\"github.com/whywaita/myshoes/api/myshoes\"\n)\n\nfunc main()  {\n\t// Set customized HTTP Client\n\tcustomHTTPClient := http.DefaultClient\n\t// Set customized logger\n\tcustomLogger := log.New(io.Discard, \"\", log.LstdFlags)\n\t\n\tclient, err := myshoes.NewClient(\"https://example.com\", customHTTPClient, customLogger)\n\tif err != nil {\n\t\t// ...\n\t}\n\t\n\ttargets, err := client.ListTarget(context.Background())\n\tif err != nil {\n\t\t// ...\n\t}\n\t\n\tfmt.Println(targets)\n}\n```"
  },
  {
    "path": "api/myshoes/client.go",
    "content": "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 client for myshoes\ntype Client struct {\n\tHTTPClient http.Client\n\tURL        *url.URL\n\n\tUserAgent string\n\tLogger    *log.Logger\n}\n\nconst (\n\tdefaultUserAgent = \"myshoes-sdk-go\"\n)\n\n// NewClient create a Client\nfunc NewClient(endpoint string, client *http.Client, logger *log.Logger) (*Client, error) {\n\tu, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse endpoint: %w\", err)\n\t}\n\n\thttpClient := client\n\tif httpClient == nil {\n\t\thttpClient = http.DefaultClient\n\t}\n\tl := logger\n\tif l == nil {\n\t\treturn &Client{\n\t\t\tHTTPClient: *httpClient,\n\t\t\tURL:        u,\n\n\t\t\t// Default is discard logger\n\t\t\tLogger: log.New(io.Discard, \"\", log.LstdFlags),\n\t\t}, nil\n\t}\n\n\treturn &Client{\n\t\tHTTPClient: *httpClient,\n\t\tURL:        u,\n\n\t\tLogger: l,\n\t}, nil\n}\n\nfunc (c *Client) newRequest(ctx context.Context, method, spath string, body io.Reader) (*http.Request, error) {\n\tu := *c.URL\n\tu.Path = path.Join(c.URL.Path, spath)\n\n\treq, err := http.NewRequestWithContext(ctx, method, u.String(), body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create a new HTTP request: %w\", err)\n\t}\n\n\tua := c.UserAgent\n\tif strings.EqualFold(ua, \"\") {\n\t\tua = defaultUserAgent\n\t}\n\treq.Header.Set(\"User-Agent\", ua)\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\treturn req, nil\n}\n\n// Error values\nvar (\n\terrCreateRequest = \"failed to create request: %w\"\n\terrRequest       = \"failed to request: %w\"\n\terrDecodeBody    = \"failed to decodeBody: %w\"\n)\n"
  },
  {
    "path": "api/myshoes/http.go",
    "content": "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 decodeBody(resp *http.Response, out interface{}) error {\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to io.ReadAll(resp.Body): %w\", err)\n\t}\n\n\tif err := json.Unmarshal(body, out); err != nil {\n\t\treturn fmt.Errorf(\"failed to json.Unmarshal() (out: %s): %w\", body, err)\n\t}\n\treturn nil\n}\n\nfunc decodeErrorBody(resp *http.Response) error {\n\tvar e web.ErrorResponse\n\n\tif err := decodeBody(resp, &e); err != nil {\n\t\treturn fmt.Errorf(errDecodeBody, err)\n\t}\n\n\treturn fmt.Errorf(\"%s\", e.Error)\n}\n\nfunc (c *Client) request(req *http.Request, out interface{}) error {\n\tc.Logger.Printf(\"Do request: %+v\", req)\n\tresp, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to do HTTP request: %w\", err)\n\t}\n\n\tswitch {\n\tcase resp.StatusCode == http.StatusNoContent:\n\t\treturn nil\n\tcase resp.StatusCode >= 400:\n\t\treturn decodeErrorBody(resp)\n\t}\n\n\tif err := decodeBody(resp, out); err != nil {\n\t\treturn fmt.Errorf(errDecodeBody, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/myshoes/target.go",
    "content": "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\"\n)\n\n// CreateTarget create a target\nfunc (c *Client) CreateTarget(ctx context.Context, param web.TargetCreateParam) (*web.UserTarget, error) {\n\tspath := \"/target\"\n\n\tjb, err := json.Marshal(param)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to json.Marshal: %w\", err)\n\t}\n\n\treq, err := c.newRequest(ctx, http.MethodPost, spath, bytes.NewBuffer(jb))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(errCreateRequest, err)\n\t}\n\n\tvar target web.UserTarget\n\tif err := c.request(req, &target); err != nil {\n\t\treturn nil, fmt.Errorf(errRequest, err)\n\t}\n\n\treturn &target, nil\n}\n\n// GetTarget get a target\nfunc (c *Client) GetTarget(ctx context.Context, targetID string) (*web.UserTarget, error) {\n\tspath := fmt.Sprintf(\"/target/%s\", targetID)\n\n\treq, err := c.newRequest(ctx, http.MethodGet, spath, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(errCreateRequest, err)\n\t}\n\n\tvar target web.UserTarget\n\tif err := c.request(req, &target); err != nil {\n\t\treturn nil, fmt.Errorf(errRequest, err)\n\t}\n\n\treturn &target, nil\n}\n\n// UpdateTarget update a target\nfunc (c *Client) UpdateTarget(ctx context.Context, targetID string, param web.TargetCreateParam) (*web.UserTarget, error) {\n\tspath := fmt.Sprintf(\"/target/%s\", targetID)\n\n\tjb, err := json.Marshal(param)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to json.Marshal: %w\", err)\n\t}\n\n\treq, err := c.newRequest(ctx, http.MethodPost, spath, bytes.NewBuffer(jb))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(errCreateRequest, err)\n\t}\n\n\tvar target web.UserTarget\n\tif err := c.request(req, &target); err != nil {\n\t\treturn nil, fmt.Errorf(errRequest, err)\n\t}\n\n\treturn &target, nil\n}\n\n// DeleteTarget delete a target\nfunc (c *Client) DeleteTarget(ctx context.Context, targetID string) error {\n\tspath := fmt.Sprintf(\"/target/%s\", targetID)\n\n\treq, err := c.newRequest(ctx, http.MethodDelete, spath, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(errCreateRequest, err)\n\t}\n\n\tvar i interface{} // this endpoint return N/A\n\tif err := c.request(req, &i); err != nil {\n\t\treturn fmt.Errorf(errRequest, err)\n\t}\n\n\treturn nil\n}\n\n// ListTarget get a list of target\nfunc (c *Client) ListTarget(ctx context.Context) ([]web.UserTarget, error) {\n\tspath := \"/target\"\n\n\treq, err := c.newRequest(ctx, http.MethodGet, spath, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(errCreateRequest, err)\n\t}\n\n\tvar targets []web.UserTarget\n\tif err := c.request(req, &targets); err != nil {\n\t\treturn nil, fmt.Errorf(errRequest, err)\n\t}\n\n\treturn targets, nil\n}\n"
  },
  {
    "path": "api/proto/myshoes.proto",
    "content": "syntax = \"proto3\";\n\npackage whywaita.myshoes;\noption go_package = \"github.com/whywaita/myshoes/api/proto.go\";\n\nservice Shoes {\n  rpc AddInstance(AddInstanceRequest) returns (AddInstanceResponse) {}\n  rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) {}\n}\n\nenum ResourceType {\n  Unknown = 0;\n  Nano = 1;\n  Micro = 2;\n  Small = 3;\n  Medium = 4;\n  Large = 5;\n  XLarge = 6;\n  XLarge2 = 7;\n  XLarge3 = 8;\n  XLarge4 = 9;\n}\n\nmessage AddInstanceRequest {\n  string runner_name = 1;\n  string setup_script = 2;\n  ResourceType resource_type = 3;\n  repeated string labels = 4;\n}\n\nmessage AddInstanceResponse {\n  string cloud_id = 1;\n  string shoes_type = 2;\n  string ip_address = 3;\n  ResourceType resource_type = 4;\n}\n\nmessage DeleteInstanceRequest {\n  string cloud_id = 1;\n  repeated string labels = 2;\n}\n\nmessage DeleteInstanceResponse {}"
  },
  {
    "path": "api/proto.go/myshoes.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        v5.29.3\n// source: myshoes.proto\n\npackage proto_go\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype ResourceType int32\n\nconst (\n\tResourceType_Unknown ResourceType = 0\n\tResourceType_Nano    ResourceType = 1\n\tResourceType_Micro   ResourceType = 2\n\tResourceType_Small   ResourceType = 3\n\tResourceType_Medium  ResourceType = 4\n\tResourceType_Large   ResourceType = 5\n\tResourceType_XLarge  ResourceType = 6\n\tResourceType_XLarge2 ResourceType = 7\n\tResourceType_XLarge3 ResourceType = 8\n\tResourceType_XLarge4 ResourceType = 9\n)\n\n// Enum value maps for ResourceType.\nvar (\n\tResourceType_name = map[int32]string{\n\t\t0: \"Unknown\",\n\t\t1: \"Nano\",\n\t\t2: \"Micro\",\n\t\t3: \"Small\",\n\t\t4: \"Medium\",\n\t\t5: \"Large\",\n\t\t6: \"XLarge\",\n\t\t7: \"XLarge2\",\n\t\t8: \"XLarge3\",\n\t\t9: \"XLarge4\",\n\t}\n\tResourceType_value = map[string]int32{\n\t\t\"Unknown\": 0,\n\t\t\"Nano\":    1,\n\t\t\"Micro\":   2,\n\t\t\"Small\":   3,\n\t\t\"Medium\":  4,\n\t\t\"Large\":   5,\n\t\t\"XLarge\":  6,\n\t\t\"XLarge2\": 7,\n\t\t\"XLarge3\": 8,\n\t\t\"XLarge4\": 9,\n\t}\n)\n\nfunc (x ResourceType) Enum() *ResourceType {\n\tp := new(ResourceType)\n\t*p = x\n\treturn p\n}\n\nfunc (x ResourceType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (ResourceType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_myshoes_proto_enumTypes[0].Descriptor()\n}\n\nfunc (ResourceType) Type() protoreflect.EnumType {\n\treturn &file_myshoes_proto_enumTypes[0]\n}\n\nfunc (x ResourceType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use ResourceType.Descriptor instead.\nfunc (ResourceType) EnumDescriptor() ([]byte, []int) {\n\treturn file_myshoes_proto_rawDescGZIP(), []int{0}\n}\n\ntype AddInstanceRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRunnerName    string                 `protobuf:\"bytes,1,opt,name=runner_name,json=runnerName,proto3\" json:\"runner_name,omitempty\"`\n\tSetupScript   string                 `protobuf:\"bytes,2,opt,name=setup_script,json=setupScript,proto3\" json:\"setup_script,omitempty\"`\n\tResourceType  ResourceType           `protobuf:\"varint,3,opt,name=resource_type,json=resourceType,proto3,enum=whywaita.myshoes.ResourceType\" json:\"resource_type,omitempty\"`\n\tLabels        []string               `protobuf:\"bytes,4,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddInstanceRequest) Reset() {\n\t*x = AddInstanceRequest{}\n\tmi := &file_myshoes_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddInstanceRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddInstanceRequest) ProtoMessage() {}\n\nfunc (x *AddInstanceRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_myshoes_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddInstanceRequest.ProtoReflect.Descriptor instead.\nfunc (*AddInstanceRequest) Descriptor() ([]byte, []int) {\n\treturn file_myshoes_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *AddInstanceRequest) GetRunnerName() string {\n\tif x != nil {\n\t\treturn x.RunnerName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddInstanceRequest) GetSetupScript() string {\n\tif x != nil {\n\t\treturn x.SetupScript\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddInstanceRequest) GetResourceType() ResourceType {\n\tif x != nil {\n\t\treturn x.ResourceType\n\t}\n\treturn ResourceType_Unknown\n}\n\nfunc (x *AddInstanceRequest) GetLabels() []string {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\ntype AddInstanceResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCloudId       string                 `protobuf:\"bytes,1,opt,name=cloud_id,json=cloudId,proto3\" json:\"cloud_id,omitempty\"`\n\tShoesType     string                 `protobuf:\"bytes,2,opt,name=shoes_type,json=shoesType,proto3\" json:\"shoes_type,omitempty\"`\n\tIpAddress     string                 `protobuf:\"bytes,3,opt,name=ip_address,json=ipAddress,proto3\" json:\"ip_address,omitempty\"`\n\tResourceType  ResourceType           `protobuf:\"varint,4,opt,name=resource_type,json=resourceType,proto3,enum=whywaita.myshoes.ResourceType\" json:\"resource_type,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AddInstanceResponse) Reset() {\n\t*x = AddInstanceResponse{}\n\tmi := &file_myshoes_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AddInstanceResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AddInstanceResponse) ProtoMessage() {}\n\nfunc (x *AddInstanceResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_myshoes_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AddInstanceResponse.ProtoReflect.Descriptor instead.\nfunc (*AddInstanceResponse) Descriptor() ([]byte, []int) {\n\treturn file_myshoes_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *AddInstanceResponse) GetCloudId() string {\n\tif x != nil {\n\t\treturn x.CloudId\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddInstanceResponse) GetShoesType() string {\n\tif x != nil {\n\t\treturn x.ShoesType\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddInstanceResponse) GetIpAddress() string {\n\tif x != nil {\n\t\treturn x.IpAddress\n\t}\n\treturn \"\"\n}\n\nfunc (x *AddInstanceResponse) GetResourceType() ResourceType {\n\tif x != nil {\n\t\treturn x.ResourceType\n\t}\n\treturn ResourceType_Unknown\n}\n\ntype DeleteInstanceRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCloudId       string                 `protobuf:\"bytes,1,opt,name=cloud_id,json=cloudId,proto3\" json:\"cloud_id,omitempty\"`\n\tLabels        []string               `protobuf:\"bytes,2,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteInstanceRequest) Reset() {\n\t*x = DeleteInstanceRequest{}\n\tmi := &file_myshoes_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteInstanceRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteInstanceRequest) ProtoMessage() {}\n\nfunc (x *DeleteInstanceRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_myshoes_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteInstanceRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteInstanceRequest) Descriptor() ([]byte, []int) {\n\treturn file_myshoes_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *DeleteInstanceRequest) GetCloudId() string {\n\tif x != nil {\n\t\treturn x.CloudId\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteInstanceRequest) GetLabels() []string {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\ntype DeleteInstanceResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteInstanceResponse) Reset() {\n\t*x = DeleteInstanceResponse{}\n\tmi := &file_myshoes_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteInstanceResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteInstanceResponse) ProtoMessage() {}\n\nfunc (x *DeleteInstanceResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_myshoes_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteInstanceResponse.ProtoReflect.Descriptor instead.\nfunc (*DeleteInstanceResponse) Descriptor() ([]byte, []int) {\n\treturn file_myshoes_proto_rawDescGZIP(), []int{3}\n}\n\nvar File_myshoes_proto protoreflect.FileDescriptor\n\nconst file_myshoes_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\rmyshoes.proto\\x12\\x10whywaita.myshoes\\\"\\xb5\\x01\\n\" +\n\t\"\\x12AddInstanceRequest\\x12\\x1f\\n\" +\n\t\"\\vrunner_name\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"runnerName\\x12!\\n\" +\n\t\"\\fsetup_script\\x18\\x02 \\x01(\\tR\\vsetupScript\\x12C\\n\" +\n\t\"\\rresource_type\\x18\\x03 \\x01(\\x0e2\\x1e.whywaita.myshoes.ResourceTypeR\\fresourceType\\x12\\x16\\n\" +\n\t\"\\x06labels\\x18\\x04 \\x03(\\tR\\x06labels\\\"\\xb3\\x01\\n\" +\n\t\"\\x13AddInstanceResponse\\x12\\x19\\n\" +\n\t\"\\bcloud_id\\x18\\x01 \\x01(\\tR\\acloudId\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"shoes_type\\x18\\x02 \\x01(\\tR\\tshoesType\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"ip_address\\x18\\x03 \\x01(\\tR\\tipAddress\\x12C\\n\" +\n\t\"\\rresource_type\\x18\\x04 \\x01(\\x0e2\\x1e.whywaita.myshoes.ResourceTypeR\\fresourceType\\\"J\\n\" +\n\t\"\\x15DeleteInstanceRequest\\x12\\x19\\n\" +\n\t\"\\bcloud_id\\x18\\x01 \\x01(\\tR\\acloudId\\x12\\x16\\n\" +\n\t\"\\x06labels\\x18\\x02 \\x03(\\tR\\x06labels\\\"\\x18\\n\" +\n\t\"\\x16DeleteInstanceResponse*\\x85\\x01\\n\" +\n\t\"\\fResourceType\\x12\\v\\n\" +\n\t\"\\aUnknown\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04Nano\\x10\\x01\\x12\\t\\n\" +\n\t\"\\x05Micro\\x10\\x02\\x12\\t\\n\" +\n\t\"\\x05Small\\x10\\x03\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06Medium\\x10\\x04\\x12\\t\\n\" +\n\t\"\\x05Large\\x10\\x05\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06XLarge\\x10\\x06\\x12\\v\\n\" +\n\t\"\\aXLarge2\\x10\\a\\x12\\v\\n\" +\n\t\"\\aXLarge3\\x10\\b\\x12\\v\\n\" +\n\t\"\\aXLarge4\\x10\\t2\\xcc\\x01\\n\" +\n\t\"\\x05Shoes\\x12\\\\\\n\" +\n\t\"\\vAddInstance\\x12$.whywaita.myshoes.AddInstanceRequest\\x1a%.whywaita.myshoes.AddInstanceResponse\\\"\\x00\\x12e\\n\" +\n\t\"\\x0eDeleteInstance\\x12'.whywaita.myshoes.DeleteInstanceRequest\\x1a(.whywaita.myshoes.DeleteInstanceResponse\\\"\\x00B*Z(github.com/whywaita/myshoes/api/proto.gob\\x06proto3\"\n\nvar (\n\tfile_myshoes_proto_rawDescOnce sync.Once\n\tfile_myshoes_proto_rawDescData []byte\n)\n\nfunc file_myshoes_proto_rawDescGZIP() []byte {\n\tfile_myshoes_proto_rawDescOnce.Do(func() {\n\t\tfile_myshoes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_myshoes_proto_rawDesc), len(file_myshoes_proto_rawDesc)))\n\t})\n\treturn file_myshoes_proto_rawDescData\n}\n\nvar file_myshoes_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_myshoes_proto_msgTypes = make([]protoimpl.MessageInfo, 4)\nvar file_myshoes_proto_goTypes = []any{\n\t(ResourceType)(0),              // 0: whywaita.myshoes.ResourceType\n\t(*AddInstanceRequest)(nil),     // 1: whywaita.myshoes.AddInstanceRequest\n\t(*AddInstanceResponse)(nil),    // 2: whywaita.myshoes.AddInstanceResponse\n\t(*DeleteInstanceRequest)(nil),  // 3: whywaita.myshoes.DeleteInstanceRequest\n\t(*DeleteInstanceResponse)(nil), // 4: whywaita.myshoes.DeleteInstanceResponse\n}\nvar file_myshoes_proto_depIdxs = []int32{\n\t0, // 0: whywaita.myshoes.AddInstanceRequest.resource_type:type_name -> whywaita.myshoes.ResourceType\n\t0, // 1: whywaita.myshoes.AddInstanceResponse.resource_type:type_name -> whywaita.myshoes.ResourceType\n\t1, // 2: whywaita.myshoes.Shoes.AddInstance:input_type -> whywaita.myshoes.AddInstanceRequest\n\t3, // 3: whywaita.myshoes.Shoes.DeleteInstance:input_type -> whywaita.myshoes.DeleteInstanceRequest\n\t2, // 4: whywaita.myshoes.Shoes.AddInstance:output_type -> whywaita.myshoes.AddInstanceResponse\n\t4, // 5: whywaita.myshoes.Shoes.DeleteInstance:output_type -> whywaita.myshoes.DeleteInstanceResponse\n\t4, // [4:6] is the sub-list for method output_type\n\t2, // [2:4] is the sub-list for method input_type\n\t2, // [2:2] is the sub-list for extension type_name\n\t2, // [2:2] is the sub-list for extension extendee\n\t0, // [0:2] is the sub-list for field type_name\n}\n\nfunc init() { file_myshoes_proto_init() }\nfunc file_myshoes_proto_init() {\n\tif File_myshoes_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_myshoes_proto_rawDesc), len(file_myshoes_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   4,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_myshoes_proto_goTypes,\n\t\tDependencyIndexes: file_myshoes_proto_depIdxs,\n\t\tEnumInfos:         file_myshoes_proto_enumTypes,\n\t\tMessageInfos:      file_myshoes_proto_msgTypes,\n\t}.Build()\n\tFile_myshoes_proto = out.File\n\tfile_myshoes_proto_goTypes = nil\n\tfile_myshoes_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "api/proto.go/myshoes_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.0\n// - protoc             v5.29.3\n// source: myshoes.proto\n\npackage proto_go\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tShoes_AddInstance_FullMethodName    = \"/whywaita.myshoes.Shoes/AddInstance\"\n\tShoes_DeleteInstance_FullMethodName = \"/whywaita.myshoes.Shoes/DeleteInstance\"\n)\n\n// ShoesClient is the client API for Shoes service.\n//\n// 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.\ntype ShoesClient interface {\n\tAddInstance(ctx context.Context, in *AddInstanceRequest, opts ...grpc.CallOption) (*AddInstanceResponse, error)\n\tDeleteInstance(ctx context.Context, in *DeleteInstanceRequest, opts ...grpc.CallOption) (*DeleteInstanceResponse, error)\n}\n\ntype shoesClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewShoesClient(cc grpc.ClientConnInterface) ShoesClient {\n\treturn &shoesClient{cc}\n}\n\nfunc (c *shoesClient) AddInstance(ctx context.Context, in *AddInstanceRequest, opts ...grpc.CallOption) (*AddInstanceResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(AddInstanceResponse)\n\terr := c.cc.Invoke(ctx, Shoes_AddInstance_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *shoesClient) DeleteInstance(ctx context.Context, in *DeleteInstanceRequest, opts ...grpc.CallOption) (*DeleteInstanceResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DeleteInstanceResponse)\n\terr := c.cc.Invoke(ctx, Shoes_DeleteInstance_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// ShoesServer is the server API for Shoes service.\n// All implementations must embed UnimplementedShoesServer\n// for forward compatibility.\ntype ShoesServer interface {\n\tAddInstance(context.Context, *AddInstanceRequest) (*AddInstanceResponse, error)\n\tDeleteInstance(context.Context, *DeleteInstanceRequest) (*DeleteInstanceResponse, error)\n\tmustEmbedUnimplementedShoesServer()\n}\n\n// UnimplementedShoesServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedShoesServer struct{}\n\nfunc (UnimplementedShoesServer) AddInstance(context.Context, *AddInstanceRequest) (*AddInstanceResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method AddInstance not implemented\")\n}\nfunc (UnimplementedShoesServer) DeleteInstance(context.Context, *DeleteInstanceRequest) (*DeleteInstanceResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method DeleteInstance not implemented\")\n}\nfunc (UnimplementedShoesServer) mustEmbedUnimplementedShoesServer() {}\nfunc (UnimplementedShoesServer) testEmbeddedByValue()               {}\n\n// UnsafeShoesServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to ShoesServer will\n// result in compilation errors.\ntype UnsafeShoesServer interface {\n\tmustEmbedUnimplementedShoesServer()\n}\n\nfunc RegisterShoesServer(s grpc.ServiceRegistrar, srv ShoesServer) {\n\t// If the following call panics, it indicates UnimplementedShoesServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Shoes_ServiceDesc, srv)\n}\n\nfunc _Shoes_AddInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(AddInstanceRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShoesServer).AddInstance(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Shoes_AddInstance_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShoesServer).AddInstance(ctx, req.(*AddInstanceRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Shoes_DeleteInstance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteInstanceRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(ShoesServer).DeleteInstance(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Shoes_DeleteInstance_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(ShoesServer).DeleteInstance(ctx, req.(*DeleteInstanceRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Shoes_ServiceDesc is the grpc.ServiceDesc for Shoes service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Shoes_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"whywaita.myshoes.Shoes\",\n\tHandlerType: (*ShoesServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"AddInstance\",\n\t\t\tHandler:    _Shoes_AddInstance_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteInstance\",\n\t\t\tHandler:    _Shoes_DeleteInstance_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"myshoes.proto\",\n}\n"
  },
  {
    "path": "cmd/server/cmd.go",
    "content": "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/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/datastore/mysql\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\t\"github.com/whywaita/myshoes/pkg/runner\"\n\t\"github.com/whywaita/myshoes/pkg/starter\"\n\t\"github.com/whywaita/myshoes/pkg/starter/safety/unlimited\"\n\t\"github.com/whywaita/myshoes/pkg/web\"\n\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc init() {\n\tconfig.Load()\n\tmysqlURL := config.LoadMySQLURL()\n\tconfig.Config.MySQLDSN = mysqlURL\n\n\tif err := gh.InitializeCache(config.Config.GitHub.AppID, config.Config.GitHub.PEMByte); err != nil {\n\t\tlog.Panicf(\"failed to create a cache: %+v\", err)\n\t}\n}\n\nfunc main() {\n\truntime.SetBlockProfileRate(1)\n\truntime.SetMutexProfileFraction(1)\n\tgo func() {\n\t\tlog.Fatal(http.ListenAndServe(\"localhost:6060\", nil))\n\t}()\n\n\tmyshoes, err := newShoes()\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\n\tif err := myshoes.Run(); err != nil {\n\t\tlog.Fatalln(err)\n\t}\n}\n\ntype myShoes struct {\n\tds    datastore.Datastore\n\tstart *starter.Starter\n\trun   *runner.Manager\n}\n\n// newShoes create myshoes.\nfunc newShoes() (*myShoes, error) {\n\tnotifyEnqueueCh := make(chan struct{}, 1)\n\n\tds, err := mysql.New(config.Config.MySQLDSN, notifyEnqueueCh)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to mysql.New: %w\", err)\n\t}\n\n\tunlimit := unlimited.Unlimited{}\n\ts := starter.New(ds, unlimit, config.Config.RunnerVersion, notifyEnqueueCh)\n\n\tmanager := runner.New(ds, config.Config.RunnerVersion)\n\n\treturn &myShoes{\n\t\tds:    ds,\n\t\tstart: s,\n\t\trun:   manager,\n\t}, nil\n}\n\n// Run start services.\nfunc (m *myShoes) Run() error {\n\teg, ctx := errgroup.WithContext(context.Background())\n\n\tfor {\n\t\tlogger.Logf(false, \"start getting lock...\")\n\t\tisLocked, err := m.ds.IsLocked(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to check lock: %w\", err)\n\t\t}\n\n\t\tif strings.EqualFold(isLocked, datastore.IsNotLocked) {\n\t\t\tif err := m.ds.GetLock(ctx); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get lock: %w\", err)\n\t\t\t}\n\n\t\t\tlogger.Logf(false, \"get lock successfully!\")\n\t\t\tbreak\n\t\t}\n\n\t\ttime.Sleep(time.Second)\n\t}\n\n\teg.Go(func() error {\n\t\tif err := web.Serve(ctx, m.ds); err != nil {\n\t\t\tlogger.Logf(false, \"failed to web.Serve: %+v\", err)\n\t\t\treturn fmt.Errorf(\"failed to serve: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\teg.Go(func() error {\n\t\tif err := m.start.Loop(ctx); err != nil {\n\t\t\tlogger.Logf(false, \"failed to starter manager: %+v\", err)\n\t\t\treturn fmt.Errorf(\"failed to starter loop: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\teg.Go(func() error {\n\t\tif err := m.run.Loop(ctx); err != nil {\n\t\t\tlogger.Logf(false, \"failed to runner manager: %+v\", err)\n\t\t\treturn fmt.Errorf(\"failed to runner loop: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err := eg.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"failed to wait errgroup: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/shoes-tester/main.go",
    "content": "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\"strconv\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-plugin\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/shoes\"\n\t\"github.com/whywaita/myshoes/pkg/starter\"\n)\n\nfunc main() {\n\tif len(os.Args) < 2 {\n\t\tprintUsage()\n\t\tos.Exit(1)\n\t}\n\n\tsubcommand := os.Args[1]\n\tswitch subcommand {\n\tcase \"add\":\n\t\tif err := runAdd(os.Args[2:]); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\tcase \"delete\":\n\t\tif err := runDelete(os.Args[2:]); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\tdefault:\n\t\tfmt.Fprintf(os.Stderr, \"Unknown subcommand: %s\\n\", subcommand)\n\t\tprintUsage()\n\t\tos.Exit(1)\n\t}\n}\n\nfunc printUsage() {\n\tfmt.Fprintf(os.Stderr, `Usage: shoes-tester <command> [options]\n\nCommands:\n  add       Add an instance\n  delete    Delete an instance\n\nRun 'shoes-tester <command> --help' for more information on a command.\n`)\n}\n\ntype addFlags struct {\n\tpluginPath           string\n\trunnerName           string\n\tresourceType         string\n\tlabels               string\n\tsetupScript          string\n\tgenerateScript       bool\n\tscope                string\n\tgithubAppID          string\n\tgithubPrivateKeyPath string\n\trunnerVersion        string\n\trunnerUser           string\n\trunnerBaseDirectory  string\n\tgithubURL            string\n\tjsonOutput           bool\n}\n\ntype deleteFlags struct {\n\tpluginPath string\n\tcloudID    string\n\tlabels     string\n\tjsonOutput bool\n}\n\nfunc runAdd(args []string) error {\n\tfs := flag.NewFlagSet(\"add\", flag.ExitOnError)\n\tflags := &addFlags{}\n\n\tfs.StringVar(&flags.pluginPath, \"plugin\", \"\", \"Path to shoes-provider binary (required)\")\n\tfs.StringVar(&flags.runnerName, \"runner-name\", \"\", \"Runner name (required)\")\n\tfs.StringVar(&flags.resourceType, \"resource-type\", \"nano\", \"Resource type (nano|micro|small|medium|large|xlarge|2xlarge|3xlarge|4xlarge)\")\n\tfs.StringVar(&flags.labels, \"labels\", \"\", \"Comma-separated labels\")\n\tfs.StringVar(&flags.setupScript, \"setup-script\", \"\", \"Setup script (simple mode)\")\n\tfs.BoolVar(&flags.generateScript, \"generate-script\", false, \"Generate setup script (script generation mode)\")\n\tfs.StringVar(&flags.scope, \"scope\", \"\", \"Repository (owner/repo) or Organization (script generation mode)\")\n\tfs.StringVar(&flags.githubAppID, \"github-app-id\", os.Getenv(\"GITHUB_APP_ID\"), \"GitHub App ID (script generation mode)\")\n\tfs.StringVar(&flags.githubPrivateKeyPath, \"github-private-key-path\", os.Getenv(\"GITHUB_PRIVATE_KEY_PATH\"), \"GitHub App private key path (script generation mode)\")\n\tfs.StringVar(&flags.runnerVersion, \"runner-version\", \"latest\", \"Runner version (script generation mode)\")\n\tfs.StringVar(&flags.runnerUser, \"runner-user\", \"runner\", \"Runner user (script generation mode)\")\n\tfs.StringVar(&flags.runnerBaseDirectory, \"runner-base-directory\", \"/tmp\", \"Runner base directory (script generation mode)\")\n\tfs.StringVar(&flags.githubURL, \"github-url\", \"\", \"GitHub Enterprise Server URL (script generation mode)\")\n\tfs.BoolVar(&flags.jsonOutput, \"json\", false, \"Output in JSON format\")\n\n\tfs.Parse(args)\n\n\tif flags.pluginPath == \"\" {\n\t\treturn fmt.Errorf(\"--plugin is required\")\n\t}\n\tif flags.runnerName == \"\" {\n\t\treturn fmt.Errorf(\"--runner-name is required\")\n\t}\n\n\tif flags.generateScript {\n\t\tif flags.scope == \"\" {\n\t\t\treturn fmt.Errorf(\"--scope is required for script generation mode\")\n\t\t}\n\t\tif flags.githubAppID == \"\" {\n\t\t\treturn fmt.Errorf(\"--github-app-id is required for script generation mode\")\n\t\t}\n\t\tif flags.githubPrivateKeyPath == \"\" {\n\t\t\treturn fmt.Errorf(\"--github-private-key-path is required for script generation mode\")\n\t\t}\n\t}\n\n\tctx := context.Background()\n\n\tvar setupScript string\n\tif flags.generateScript {\n\t\tscript, err := generateSetupScript(ctx, flags)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to generate setup script: %w\", err)\n\t\t}\n\t\tsetupScript = script\n\t} else {\n\t\tsetupScript = flags.setupScript\n\t}\n\n\tresourceType := datastore.UnmarshalResourceTypeString(flags.resourceType)\n\tif resourceType == datastore.ResourceTypeUnknown && flags.resourceType != \"unknown\" {\n\t\treturn fmt.Errorf(\"invalid resource type: %s\", flags.resourceType)\n\t}\n\n\tlabels := parseLabels(flags.labels)\n\n\tclient, teardown, err := getClientWithPath(flags.pluginPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get plugin client: %w\", err)\n\t}\n\tdefer teardown()\n\n\tcloudID, ipAddress, shoesType, actualResourceType, err := client.AddInstance(ctx, flags.runnerName, setupScript, resourceType, labels)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add instance: %w\", err)\n\t}\n\n\tif flags.jsonOutput {\n\t\toutput := map[string]string{\n\t\t\t\"cloud_id\":      cloudID,\n\t\t\t\"ip_address\":    ipAddress,\n\t\t\t\"shoes_type\":    shoesType,\n\t\t\t\"resource_type\": actualResourceType.String(),\n\t\t}\n\t\tdata, err := json.Marshal(output)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal JSON: %w\", err)\n\t\t}\n\t\tfmt.Println(string(data))\n\t} else {\n\t\tfmt.Printf(\"AddInstance succeeded:\\n\")\n\t\tfmt.Printf(\"  Cloud ID:      %s\\n\", cloudID)\n\t\tfmt.Printf(\"  Shoes Type:    %s\\n\", shoesType)\n\t\tfmt.Printf(\"  IP Address:    %s\\n\", ipAddress)\n\t\tfmt.Printf(\"  Resource Type: %s\\n\", actualResourceType.String())\n\t}\n\n\treturn nil\n}\n\nfunc runDelete(args []string) error {\n\tfs := flag.NewFlagSet(\"delete\", flag.ExitOnError)\n\tflags := &deleteFlags{}\n\n\tfs.StringVar(&flags.pluginPath, \"plugin\", \"\", \"Path to shoes-provider binary (required)\")\n\tfs.StringVar(&flags.cloudID, \"cloud-id\", \"\", \"Cloud ID (required)\")\n\tfs.StringVar(&flags.labels, \"labels\", \"\", \"Comma-separated labels\")\n\tfs.BoolVar(&flags.jsonOutput, \"json\", false, \"Output in JSON format\")\n\n\tfs.Parse(args)\n\n\tif flags.pluginPath == \"\" {\n\t\treturn fmt.Errorf(\"--plugin is required\")\n\t}\n\tif flags.cloudID == \"\" {\n\t\treturn fmt.Errorf(\"--cloud-id is required\")\n\t}\n\n\tctx := context.Background()\n\n\tlabels := parseLabels(flags.labels)\n\n\tclient, teardown, err := getClientWithPath(flags.pluginPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get plugin client: %w\", err)\n\t}\n\tdefer teardown()\n\n\tif err := client.DeleteInstance(ctx, flags.cloudID, labels); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete instance: %w\", err)\n\t}\n\n\tif flags.jsonOutput {\n\t\toutput := map[string]string{\n\t\t\t\"status\": \"success\",\n\t\t}\n\t\tdata, err := json.Marshal(output)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal JSON: %w\", err)\n\t\t}\n\t\tfmt.Println(string(data))\n\t} else {\n\t\tfmt.Printf(\"DeleteInstance succeeded\\n\")\n\t}\n\n\treturn nil\n}\n\nfunc getClientWithPath(pluginPath string) (shoes.Client, func(), error) {\n\thandshake := plugin.HandshakeConfig{\n\t\tProtocolVersion:  1,\n\t\tMagicCookieKey:   \"SHOES_PLUGIN_MAGIC_COOKIE\",\n\t\tMagicCookieValue: \"are_you_a_shoes?\",\n\t}\n\tpluginMap := map[string]plugin.Plugin{\n\t\t\"shoes_grpc\": &shoes.Plugin{},\n\t}\n\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig:  handshake,\n\t\tPlugins:          pluginMap,\n\t\tCmd:              exec.Command(pluginPath),\n\t\tManaged:          true,\n\t\tStderr:           os.Stderr,\n\t\tSyncStdout:       os.Stdout,\n\t\tSyncStderr:       os.Stderr,\n\t\tAllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},\n\t})\n\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get shoes client: %w\", err)\n\t}\n\n\traw, err := rpcClient.Dispense(\"shoes_grpc\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to shoes client instance: %w\", err)\n\t}\n\n\treturn raw.(shoes.Client), client.Kill, nil\n}\n\nfunc generateSetupScript(ctx context.Context, flags *addFlags) (string, error) {\n\tkeyBytes, err := os.ReadFile(flags.githubPrivateKeyPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read private key: %w\", err)\n\t}\n\n\tappID, err := strconv.ParseInt(flags.githubAppID, 10, 64)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse github-app-id: %w\", err)\n\t}\n\n\tblock, _ := pem.Decode(keyBytes)\n\tif block == nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode PEM block from private key\")\n\t}\n\n\tprivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse private key: %w\", err)\n\t}\n\n\tconfig.Config.GitHub.AppID = appID\n\tconfig.Config.GitHub.PEMByte = keyBytes\n\tconfig.Config.GitHub.PEM = privateKey\n\n\tif flags.githubURL == \"\" {\n\t\tconfig.Config.GitHubURL = \"https://github.com\"\n\t} else {\n\t\tconfig.Config.GitHubURL = flags.githubURL\n\t}\n\n\tconfig.Config.RunnerUser = flags.runnerUser\n\tconfig.Config.RunnerBaseDirectory = flags.runnerBaseDirectory\n\n\tif err := gh.InitializeCache(appID, keyBytes); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to initialize GitHub client cache: %w\", err)\n\t}\n\n\ts := starter.New(nil, nil, flags.runnerVersion, nil)\n\treturn s.GetSetupScript(ctx, flags.scope, flags.runnerName)\n}\n\nfunc parseLabels(labels string) []string {\n\tif labels == \"\" {\n\t\treturn []string{}\n\t}\n\tparts := strings.Split(labels, \",\")\n\tresult := make([]string, 0, len(parts))\n\tfor _, p := range parts {\n\t\ttrimmed := strings.TrimSpace(p)\n\t\tif trimmed != \"\" {\n\t\t\tresult = append(result, trimmed)\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "docs/01_01_for_admin_setup.md",
    "content": "# Setup myshoes daemon\n\n## Goal\n\n- Start myshoes daemon\n\n## Prepare\n\n- The network connectivity to myshoes server.\n  - The webhook endpoint from github.com **OR** your GitHub Enterprise Server (`POST /github/events`).\n  - REST API from your workspace (`GET, POST, DELETE /target`).\n- You decide platform for runner and shoes-provider\n  - The official shoes-provider topic is [myshoes-provider](https://github.com/search?q=topic%3Amyshoes-provider).\n  - You can implement and use your private shoes-provider. Please check [how-to-develop-shoes.md](./03_how-to-develop-shoes.md).\n\n## Word definition\n\n- `your_shoes_host`: The endpoint of serving myshoes.\n  - e.g.) `https://myshoes.example.com`\n\n## Setup\n\nPlease prepare a few things first.\n\n### Machine image for runner\n\n- Virtual Machine Image on your cloud provider.\n  - installed a some commands.\n    - required: curl (1)\n    - optional: jq (1), docker (1)\n      - optional, but **STRONG RECOMMEND INSTALLING BEFORE** (please read known issue)\n  - put latest runner tar.gz to `/usr/local/etc` [optional]\n    - optional, but **STRONG RECOMMEND INSTALLING BEFORE** (please read known issue)\n\nFor example is [here](https://github.com/whywaita/myshoes-providers/tree/master/shoes-lxd/images). (packer file)\n\n### Create GitHub Apps\n\n#### Configure values\n\n- GitHub App Name: any text\n- Homepage URL: any text\n  \n##### Webhook\n- Webhook URL: `${your_shoes_host}/github/events`\n- Webhook secret: any text\n\n##### Repository permissions\n\n- Actions: Read-only\n- Administration: Read & write\n- Checks: Read-only\n\n##### Organization permissions\n\n- Self-hosted runners: Read & write\n  \n##### Subscribe to events\n\n- Check `Workflow job`\n\n### Download private key\n\n- download from GitHub or upload private key from your machine.\n\n### Running\n\n```bash\n$ make build\n$ ./myshoes\n```\n\nA config variables can set from environment values.\n\n- `PORT`\n  - default: 8080\n  - Listen port for myshoes.\n- GitHub Apps information\n  - required\n  - `GITHUB_APP_ID`\n  - `GITHUB_APP_SECRET` (if you set `Webhook secret` for your GitHub App)\n  - `GITHUB_PRIVATE_KEY_BASE64`\n    - base64 encoded private key from GitHub Apps\n    - `$ cat privatekey.pem | base64 -w 0`\n- `MYSQL_URL`\n  - required\n  - DataSource Name, ex) `username:password@tcp(localhost:3306)/myshoes`\n  - if `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE` all are set, this env value are ignored.\n- `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_DATABASE`\n  - optional\n  - If all environment variables are set, mysql_url is constructed and loaded in the following way, then `MYSQL_URL` env will be ignored.\n  - example) `${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DATABASE}`\n- `PLUGIN`\n  - required\n  - set path of myshoes-provider binary.\n  - example) `./shoes-mock` `https://example.com/shoes-mock` `https://github.com/whywaita/myshoes-providers/releases/download/v0.1.0/shoes-lxd-linux-amd64`\n- `PLUGIN_OUTPUT`\n  - default: `.`\n  - set path of directory that contains myshoes-provider binary.\n- `GITHUB_URL`\n  - default: `https://github.com`\n  - The URL of GitHub Enterprise Server.\n  - Please contain schema.\n- `RUNNER_VERSION`\n  - default: `latest`\n    - Use the latest version in starting job\n  - The version of `actions/runner`\n  - example) `v2.302.1`, `latest`\n- `RUNNER_USER`\n  - default: `runner`\n  - set linux username that executes runner. you need to set exist user.\n    - DO NOT set root. It can't run GitHub Actions runner in root permission.\n    - Example: `ubuntu`\n- `PROVIDE_DOCKER_HUB_METRICS`\n  - default: `false`\n  - set `true` if you want to provide rate-limit metrics for Docker Hub.\n  - If you're not anonymous user, you need to set `DOCKER_HUB_USERNAME` and `DOCKER_HUB_PASSWORD`.\n- `DOCKER_HUB_USERNAME`\n  - default: `` (empty)\n  - set Docker Hub username for pulling Docker image. (Use for provide rate-limit metrics)\n- `DOCKER_HUB_PASSWORD`\n  - default: `` (empty)\n  - set Docker Hub password for pulling Docker image. (Use for provide rate-limit metrics)\n\n\nFor tuning values\n\n- `DEBUG`\n  - default: false\n  - show debugging log\n- `STRICT`\n  - default: true\n  - set strict mode\n- `MODE_WEBHOOK_TYPE`\n  - default: `workflow_job` (use receive `workflow_job` event)\n  - Set type of webhook from GitHub\n  - option: `check_run`\n- `MAX_CONNECTIONS_TO_BACKEND`\n  - default: 50\n  - The number of max connections to shoes-provider\n- `MAX_CONCURRENCY_DELETING`\n  - default: 1\n  - The number of max concurrency of deleting\n\nand more some env values from [shoes provider](https://github.com/search?q=topic%3Amyshoes-provider).\n"
  },
  {
    "path": "docs/01_02_for_admin_tips.md",
    "content": "# Tips for myshoes admin\n\n## Job management hooks for self-hosted runners\n\nYou can use job management hooks for self-hosted runners.\nPlease set script file to your runner image.\n\n- `ACTIONS_RUNNER_HOOK_JOB_STARTED`: `/myshoes-actions-runner-hook-job-started.sh`\n- `ACTIONS_RUNNER_HOOK_JOB_COMPLETED`: `/myshoes-actions-runner-hook-job-completed.sh`"
  },
  {
    "path": "docs/02_01_for_user_setup.md",
    "content": "# 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  - e.g.) `https://github.com/apps/<GitHub Apps name>` or `<GHES url>/github-apps/<GitHub Apps name>`\n- Get Endpoint for myshoes from myshoes admin.\n  - e.g.) `<your_shoes_host>/target`\n\n## Repository or Organization setup\n\n### Install GitHub Apps\n\nPlease open GitHub Apps's Public page and install GitHub Apps to Organization or repository.\n\n![](./assets/img/02_01_githubapps_publicpage.png)\n\n![](./assets/img/02_01_githubapps_installpage.png)\n\n### Register target to myshoes\n\nyou need to register a target that repository or organization.\n\n- `scope`: set target scope for an auto-scaling runner.\n  - Repository example: `octocat/hello-worlds`\n  - Organization example: `octocat`\n- `resource_type`: set instance size for a runner.\n  - We will describe later.\n  - Please teach it from myshoes admin.\n\nExample (create a target):\n\n```bash\n$ curl -XPOST -d '{\"scope\": \"octocat/hello-world\", \"resource_type\": \"micro\"}' ${your_shoes_host}/target\n```\n\nYou can check registered targets.\n\n```bash\ncurl -XGET ${your_shoes_host}/target | jq .\n[\n  {\n    \"id\": \"477f6073-90d1-47d8-958f-4707cea61e8d\",\n    \"scope\": \"octocat\",\n    \"token_expired_at\": \"2006-01-02T15:04:05Z\",\n    \"resource_type\": \"micro\",\n    \"provider_url\": \"\",\n    \"status\": \"active\",\n    \"status_description\": \"\",\n    \"created_at\": \"2006-01-02T15:04:05Z\",\n    \"updated_at\": \"2006-01-02T15:04:05Z\"\n  }\n]\n```\n\n#### Switch `resource_type`\n\nYou can set `resource_type` in target. So myshoes switch size of instance.\n\nFor example,\n\n- In organization scope (`octocat`), want to set small size runner as a `nano`.\n- But specific repository (`octocat/huge-repository`), want to set big size runner as a `4xlarge`.\n\nSo please configure it.\n\n```bash\n$ curl -XPOST -d '{\"scope\": \"octocat\", \"resource_type\": \"nano\"}' ${your_shoes_host}/target\n$ curl -XPOST -d '{\"scope\": \"octocat/huge-repository\", \"resource_type\": \"4xlarge\"}' ${your_shoes_host}/target\n\n$ curl -XGET ${your_shoes_host}/target | jq .\n[\n  {\n    \"id\": \"477f6073-90d1-47d8-958f-4707cea61e8d\",\n    \"scope\": \"octocat\",\n    \"token_expired_at\": \"2006-01-02T15:04:05Z\",\n    \"resource_type\": \"nano\",\n    \"provider_url\": \"\",\n    \"status\": \"active\",\n    \"status_description\": \"\",\n    \"created_at\": \"2006-01-02T15:04:05Z\",\n    \"updated_at\": \"2006-01-02T15:04:05Z\"\n  },\n  {\n    \"id\": \"3775e3b6-08e0-4abc-830d-fd5325397de0\",\n    \"scope\": \"octocat/huge-repository\",\n    \"token_expired_at\": \"2006-01-02T15:04:05Z\",\n    \"resource_type\": \"4xlarge\",\n    \"provider_url\": \"\",\n    \"status\": \"active\",\n    \"status_description\": \"\",\n    \"created_at\": \"2006-01-02T15:04:05Z\",\n    \"updated_at\": \"2006-01-02T15:04:05Z\"\n  }\n]\n```\n\nIn this configuration, myshoes will create under it.\n\n- In `octocat/normal-repository`, will create `nano`\n- In `octocat/normal-repository2`, will create `nano`\n- In `octocat/huge-repository`, will create `4xlarge`\n\n### Create an offline runner (only use `check_run` mode)\n\nGitHub Actions need offline runner if queueing job.\nPlease create an offline runner in the target repository.\n\nhttps://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners/adding-self-hosted-runners\n\nPlease delete a runner after registered.\n\nAfter that, You can use [cycloud-io/refresh-runner-action](https://github.com/cycloud-io/refresh-runner-action) for automation.\n\n### Let's go using your shoes!\n\nLet's execute your jobs! :runner::runner::runner:\n"
  },
  {
    "path": "docs/03_how-to-develop-shoes.md",
    "content": "# How to develop shoes provider\n\n## TL;DR\n\n- implement to gRPC server\n    - `shoes`, `health`, `stdio`\n- define resource type in your shoes provider's flavor.\n\n## gRPC server\n\nshoes provider use [hashicorp/go-plugin](https://github.com/hashicorp/go-plugin).\nyou need to register three Service.\n\nif you use a golang in development, you can use `pkg/pluginutils/setup.go`.\n\nplease check `plugins/shoes-mock`. There are mock shoes provider.\n\n### shoes server\n\n`shoes` is gRPC server. you need to implement two funtion.\n\n- `AddInstance`\n- `DeleteInstance`\n\nplease check `api/proto/myshoes.proto`.\n\n### health\n\n`health` is [grpc-ecosystem/grpc-health-probe](https://github.com/grpc-ecosystem/grpc-health-probe).\n\n### stdio\n\n`stdio` is standard I/O service.\n\nthis service communicate plugin binary's standard I/O. \n\n## Resource type\n\nmyshoes defined some machine type. you need to map machine spec for your resource type.\n\n- nano\n- micro\n- small\n- medium\n- large\n- xlarge\n- 2xlarge\n- 3xlarge\n- 4xlarge\n\n## Testing shoes provider\n\n`shoes-tester` is a CLI tool for testing shoes provider without running myshoes server.\n\n### Build\n\n```bash\ngo build -o shoes-tester ./cmd/shoes-tester\n```\n\n### Usage\n\n#### Add instance\n\nSimple mode (with setup script):\n\n```bash\n./shoes-tester add \\\n  --plugin ./path/to/your-shoes-provider \\\n  --runner-name test-runner \\\n  --resource-type nano \\\n  --labels \"label1,label2\" \\\n  --setup-script \"#!/bin/bash\\necho 'setup'\"\n```\n\nScript generation mode (generate setup script automatically):\n\n```bash\n./shoes-tester add \\\n  --plugin ./path/to/your-shoes-provider \\\n  --runner-name test-runner \\\n  --resource-type nano \\\n  --generate-script \\\n  --scope owner/repo \\\n  --github-app-id 123456 \\\n  --github-private-key-path /path/to/key.pem \\\n  --runner-version latest\n```\n\n#### Delete instance\n\n```bash\n./shoes-tester delete \\\n  --plugin ./path/to/your-shoes-provider \\\n  --cloud-id your-cloud-id \\\n  --labels \"label1,label2\"\n```\n\n#### Options\n\nAdd command:\n- `--plugin`: Path to shoes-provider binary (required)\n- `--runner-name`: Runner name (required)\n- `--resource-type`: Resource type (default: nano)\n- `--labels`: Comma-separated labels\n- `--setup-script`: Setup script (simple mode)\n- `--generate-script`: Generate setup script automatically (script generation mode)\n- `--scope`: Repository (owner/repo) or Organization (script generation mode)\n- `--github-app-id`: GitHub App ID (script generation mode)\n- `--github-private-key-path`: GitHub App private key path (script generation mode)\n- `--runner-version`: Runner version (default: latest, script generation mode)\n- `--runner-user`: Runner user (default: runner, script generation mode)\n- `--runner-base-directory`: Runner base directory (default: /tmp, script generation mode)\n- `--github-url`: GitHub Enterprise Server URL (script generation mode)\n- `--json`: Output in JSON format\n\nDelete command:\n- `--plugin`: Path to shoes-provider binary (required)\n- `--cloud-id`: Cloud ID (required)\n- `--labels`: Comma-separated labels\n- `--json`: Output in JSON format"
  },
  {
    "path": "docs/assets/myshoes.service",
    "content": "[Unit]\nDescription=myshoes is Auto scaling self-hosted runner :runner: (like GitHub-hosted) for GitHub Actions\nAfter=network.target\n\n[Service]\nUser=root\nEnvironmentFile=/etc/default/myshoes\nExecStart=/usr/local/bin/myshoes\nRestart=always\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "go.mod",
    "content": "module github.com/whywaita/myshoes\n\ngo 1.25\n\nrequire (\n\tgithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0\n\tgithub.com/go-sql-driver/mysql v1.9.3\n\tgithub.com/golang-jwt/jwt/v4 v4.5.2\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-github/v80 v80.0.0\n\tgithub.com/hashicorp/go-plugin v1.7.0\n\tgithub.com/hashicorp/go-version v1.8.0\n\tgithub.com/jmoiron/sqlx v1.4.0\n\tgithub.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f\n\tgithub.com/ory/dockertest/v3 v3.12.0\n\tgithub.com/patrickmn/go-cache v2.1.0+incompatible\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/r3labs/diff/v2 v2.15.1\n\tgithub.com/satori/go.uuid v1.2.0\n\tgoji.io v2.0.2+incompatible\n\tgolang.org/x/oauth2 v0.32.0\n\tgolang.org/x/sync v0.18.0\n\tgoogle.golang.org/grpc v1.77.0\n\tgoogle.golang.org/protobuf v1.36.10\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/continuity v0.4.5 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/cli v29.1.2+incompatible // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/google/go-github/v75 v75.0.0 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/moby/api v1.52.0 // indirect\n\tgithub.com/moby/moby/client v0.2.1 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/oklog/run v1.2.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/opencontainers/runc v1.2.3 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.66.1 // indirect\n\tgithub.com/prometheus/procfs v0.16.1 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/vmihailenco/msgpack v4.0.4+incompatible // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect\n\tgo.opentelemetry.io/otel v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.38.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.38.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.2 // indirect\n\tgolang.org/x/net v0.47.0 // indirect\n\tgolang.org/x/sys v0.38.0 // indirect\n\tgolang.org/x/text v0.31.0 // indirect\n\tgoogle.golang.org/appengine v1.6.8 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg=\ngithub.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY=\ngithub.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=\ngithub.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/cli v29.1.2+incompatible h1:s4QI7drXpIo78OM+CwuthPsO5kCf8cpNsck5PsLVTH8=\ngithub.com/docker/cli v29.1.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=\ngithub.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=\ngithub.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=\ngithub.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs=\ngithub.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=\ngithub.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=\ngithub.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=\ngithub.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=\ngithub.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f h1:MBcrTbmCf7CZa9yAwcB7ArveQb9TPVy4zFnQGz/LiUU=\ngithub.com/m4ns0ur/httpcache v0.0.0-20200426190423-1040e2e8823f/go.mod h1:UawoqorwkpZ58qWiL+nVJM0Po7FrzAdCxYVh9GgTTaA=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=\ngithub.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=\ngithub.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=\ngithub.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80=\ngithub.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM=\ngithub.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=\ngithub.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=\ngithub.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=\ngithub.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg=\ngithub.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=\ngithub.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=\ngo.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=\ngo.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=\ngo.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=\ngo.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=\ngo.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=\ngo.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=\ngo.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=\ngo.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=\ngo.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=\ngo.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=\ngoji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=\ngoji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=\ngolang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=\ngolang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=\ngolang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=\ngolang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=\ngolang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=\ngoogle.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=\ngoogle.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "internal/testutils/mysql.go",
    "content": "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\"github.com/jmoiron/sqlx\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\nconst schemaDirRelativePathFormat = \"%s/../../pkg/datastore/mysql/%s\"\n\nfunc execSchema(fpath string) {\n\tb, err := os.ReadFile(fpath)\n\tif err != nil {\n\t\tlog.Fatalf(\"schema reading error: %v\", err)\n\t}\n\n\tqueries := strings.Split(string(b), \";\")\n\n\tfor _, query := range queries[:len(queries)-1] {\n\t\t_, err = testDB.Exec(query)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"exec schema error: %v, query: %s\", err, query)\n\t\t}\n\t}\n}\n\nfunc createTablesIfNotExist() {\n\t_, pwd, _, _ := runtime.Caller(0)\n\tschemaPath := fmt.Sprintf(schemaDirRelativePathFormat, path.Dir(pwd), \"schema.sql\")\n\texecSchema(schemaPath)\n}\n\nfunc truncateTables() {\n\trows, err := testDB.Query(\"SHOW TABLES\")\n\tif err != nil {\n\t\tlog.Fatalf(\"show tables error: %#v\", err)\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar tableName string\n\t\terr = rows.Scan(&tableName)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"show table error: %#v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tcmds := []string{\n\t\t\t\"SET FOREIGN_KEY_CHECKS = 0\",\n\t\t\tfmt.Sprintf(\"TRUNCATE %s\", tableName),\n\t\t\t\"SET FOREIGN_KEY_CHECKS = 1\",\n\t\t}\n\t\tfor _, cmd := range cmds {\n\t\t\t_, err := testDB.Exec(cmd)\n\n\t\t\tif err != nil {\n\t\t\t\tmysqlErr, ok := err.(*mysql.MySQLError)\n\n\t\t\t\tif ok {\n\t\t\t\t\tif mysqlErr.Number == 0xde2 {\n\t\t\t\t\t\t// is rejected\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tlog.Fatalf(\"truncate error: %#v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// GetTestDatastore return pointer of datastore\nfunc GetTestDatastore() (datastore.Datastore, func()) {\n\tif testDatastore == nil {\n\t\tpanic(\"datastore is not initialized yet\")\n\t}\n\n\treturn testDatastore, func() { truncateTables() }\n}\n\n// GetTestDB return pointer of testDB\nfunc GetTestDB() (*sqlx.DB, func()) {\n\tif testDB == nil {\n\t\tpanic(\"testDB is not initialized yet\")\n\t}\n\n\treturn testDB, func() { truncateTables() }\n}\n"
  },
  {
    "path": "internal/testutils/testutils.go",
    "content": "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\t\"github.com/whywaita/myshoes/pkg/datastore/mysql\"\n\t\"github.com/whywaita/myshoes/pkg/web\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/ory/dockertest/v3\"\n)\n\nconst (\n\tmysqlRootPassword = \"secret\"\n)\n\nvar (\n\ttestDB        *sqlx.DB\n\ttestDatastore datastore.Datastore\n\n\ttestURL string\n)\n\n// IntegrationTestRunner is all integration test\nfunc IntegrationTestRunner(m *testing.M) int {\n\t// uses a sensible default on windows (tcp/http) and linux/osx (socket)\n\tpool, err := dockertest.NewPool(\"\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not connect to docker: %s\", err)\n\t}\n\n\t// pulls an image, creates a container based on it and runs it\n\tresource, err := pool.Run(\"mysql\", \"8.0\", []string{\"MYSQL_ROOT_PASSWORD=\" + mysqlRootPassword})\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not start resource: %s\", err)\n\t}\n\n\t// exponential backoff-retry, because the application in the container might not be ready to accept connections yet\n\tif err := pool.Retry(func() error {\n\t\tvar err error\n\t\tdsn := fmt.Sprintf(\"root:%s@(localhost:%s)/mysql\", mysqlRootPassword, resource.GetPort(\"3306/tcp\"))\n\t\ttestDatastore, err = mysql.New(dsn, make(chan<- struct{}))\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to create datastore instance: %s\", err)\n\t\t}\n\n\t\ttestDB, err = sqlx.Open(\"mysql\", fmt.Sprintf(\"root:%s@(localhost:%s)/mysql?parseTime=true&loc=UTC\", mysqlRootPassword, resource.GetPort(\"3306/tcp\")))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn testDB.Ping()\n\t}); err != nil {\n\t\tlog.Fatalf(\"Could not connect to docker: %s\", err)\n\t}\n\n\tcreateTablesIfNotExist()\n\t//SetupDefaultFixtures()\n\n\tmux := web.NewMux(testDatastore)\n\tts := httptest.NewServer(mux)\n\ttestURL = ts.URL\n\n\tcode := m.Run()\n\n\tts.Close()\n\ttruncateTables()\n\n\t// You can't defer this because os.Exit doesn't care for defer\n\tif err := pool.Purge(resource); err != nil {\n\t\tlog.Fatalf(\"Could not purge resource: %s\", err)\n\t}\n\n\treturn code\n}\n"
  },
  {
    "path": "internal/testutils/web.go",
    "content": "package testutils\n\n// GetTestURL return url of httptest.Server\nfunc GetTestURL() string {\n\tif testURL == \"\" {\n\t\tpanic(\"testURL is not initialized yet\")\n\t}\n\n\treturn testURL\n}\n"
  },
  {
    "path": "internal/util/util.go",
    "content": "package util\n\nimport (\n\t\"math/rand/v2\"\n\t\"time\"\n)\n\n// CalcRetryTime is caliculate retry time by exponential backoff and jitter\nfunc CalcRetryTime(count int) time.Duration {\n\tif count == 0 {\n\t\treturn 0\n\t}\n\n\tbackoff := 1 << count\n\tjitter := time.Duration(rand.IntN(1000)) * time.Millisecond\n\n\treturn time.Duration(backoff)*time.Second + jitter\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "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 Config\ntype Conf struct {\n\tGitHub GitHubApp\n\n\tMySQLDSN              string\n\tPort                  int\n\tShoesPluginPath       string\n\tShoesPluginOutputPath string\n\tRunnerUser            string\n\tRunnerBaseDirectory   string\n\n\tDebug           bool\n\tStrict          bool // check to registered runner before delete job\n\tModeWebhookType ModeWebhookType\n\n\tMaxConnectionsToBackend int64\n\tMaxConcurrencyDeleting  int64\n\n\tGitHubURL     string\n\tRunnerVersion string\n\n\tDockerHubCredential     DockerHubCredential\n\tProvideDockerHubMetrics bool\n}\n\n// DockerHubCredential is type of config value\ntype DockerHubCredential struct {\n\tUsername string\n\tPassword string\n}\n\n// GitHubApp is type of config value\ntype GitHubApp struct {\n\tAppID     int64\n\tAppSecret []byte\n\tPEMByte   []byte\n\tPEM       *rsa.PrivateKey\n}\n\n// Config Environment keys\nconst (\n\tEnvGitHubAppID               = \"GITHUB_APP_ID\"\n\tEnvGitHubAppSecret           = \"GITHUB_APP_SECRET\"\n\tEnvGitHubAppPrivateKeyBase64 = \"GITHUB_PRIVATE_KEY_BASE64\"\n\tEnvMySQLHost                 = \"MYSQL_HOST\"\n\tEnvMySQLPort                 = \"MYSQL_PORT\"\n\tEnvMySQLUser                 = \"MYSQL_USER\"\n\tEnvMySQLPassword             = \"MYSQL_PASSWORD\"\n\tEnvMySQLDatabase             = \"MYSQL_DATABASE\"\n\tEnvMySQLURL                  = \"MYSQL_URL\"\n\tEnvPort                      = \"PORT\"\n\tEnvShoesPluginPath           = \"PLUGIN\"\n\tEnvShoesPluginOutputPath     = \"PLUGIN_OUTPUT\"\n\tEnvRunnerUser                = \"RUNNER_USER\"\n\tEnvRunnerBaseDirectory       = \"RUNNER_BASE_DIRECTORY\"\n\tEnvDebug                     = \"DEBUG\"\n\tEnvStrict                    = \"STRICT\"\n\tEnvModeWebhookType           = \"MODE_WEBHOOK_TYPE\"\n\tEnvMaxConnectionsToBackend   = \"MAX_CONNECTIONS_TO_BACKEND\"\n\tEnvMaxConcurrencyDeleting    = \"MAX_CONCURRENCY_DELETING\"\n\tEnvGitHubURL                 = \"GITHUB_URL\"\n\tEnvRunnerVersion             = \"RUNNER_VERSION\"\n\tEnvDockerHubUsername         = \"DOCKER_HUB_USERNAME\"\n\tEnvDockerHubPassword         = \"DOCKER_HUB_PASSWORD\"\n\tEnvProvideDockerHubMetrics   = \"PROVIDE_DOCKER_HUB_METRICS\"\n)\n\n// ModeWebhookType is type value for GitHub webhook\ntype ModeWebhookType int\n\nconst (\n\t// ModeWebhookTypeUnknown is unknown\n\tModeWebhookTypeUnknown ModeWebhookType = iota\n\t// ModeWebhookTypeCheckRun is check_run\n\tModeWebhookTypeCheckRun\n\t// ModeWebhookTypeWorkflowJob is workflow_job\n\tModeWebhookTypeWorkflowJob\n)\n\n// String is implementation of fmt.Stringer\nfunc (mwt ModeWebhookType) String() string {\n\tunknown := \"unknown\"\n\tswitch mwt {\n\tcase ModeWebhookTypeUnknown:\n\t\treturn unknown\n\tcase ModeWebhookTypeCheckRun:\n\t\treturn \"check_run\"\n\tcase ModeWebhookTypeWorkflowJob:\n\t\treturn \"workflow_job\"\n\t}\n\n\treturn unknown\n}\n\n// Equal check in and value\nfunc (mwt ModeWebhookType) Equal(in string) bool {\n\treturn strings.EqualFold(in, mwt.String())\n}\n\nfunc marshalModeWebhookType(in string) ModeWebhookType {\n\tswitch in {\n\tcase \"check_run\":\n\t\treturn ModeWebhookTypeCheckRun\n\tcase \"workflow_job\":\n\t\treturn ModeWebhookTypeWorkflowJob\n\t}\n\n\treturn ModeWebhookTypeUnknown\n}\n\n// IsGHES return myshoes for GitHub Enterprise Server\nfunc (c Conf) IsGHES() bool {\n\treturn !strings.EqualFold(c.GitHubURL, \"https://github.com\")\n}\n"
  },
  {
    "path": "pkg/config/init.go",
    "content": "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\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-version\"\n)\n\n// Load load config from environment\nfunc Load() {\n\tc := LoadWithDefault()\n\n\tga := LoadGitHubApps()\n\tc.GitHub = *ga\n\n\tpluginPath := LoadPluginPath()\n\tc.ShoesPluginPath = pluginPath\n\n\tConfig = c\n}\n\n// LoadWithDefault load only value that has default value\nfunc LoadWithDefault() Conf {\n\tvar c Conf\n\n\tp := \"8080\"\n\tif os.Getenv(EnvPort) != \"\" {\n\t\tp = os.Getenv(EnvPort)\n\t}\n\tpp, err := strconv.Atoi(p)\n\tif err != nil {\n\t\tlog.Panicf(\"failed to parse PORT: %+v\", err)\n\t}\n\tc.Port = pp\n\n\trunnerUser := \"runner\"\n\tif os.Getenv(EnvRunnerUser) != \"\" {\n\t\trunnerUser = os.Getenv(EnvRunnerUser)\n\t}\n\tc.RunnerUser = runnerUser\n\n\tc.RunnerBaseDirectory = \"/tmp\"\n\tif os.Getenv(EnvRunnerBaseDirectory) != \"\" {\n\t\tc.RunnerBaseDirectory = os.Getenv(EnvRunnerBaseDirectory)\n\t\tlog.Printf(\"use runner base directory is %s\\n\", c.RunnerBaseDirectory)\n\t}\n\n\tc.Debug = false\n\tif os.Getenv(EnvDebug) == \"true\" {\n\t\tc.Debug = true\n\t}\n\n\tc.Strict = true\n\tif os.Getenv(EnvStrict) == \"false\" {\n\t\tc.Strict = false\n\t}\n\n\tc.ModeWebhookType = ModeWebhookTypeWorkflowJob\n\tif os.Getenv(EnvModeWebhookType) != \"\" {\n\t\tmwt := marshalModeWebhookType(os.Getenv(EnvModeWebhookType))\n\n\t\tif mwt == ModeWebhookTypeUnknown {\n\t\t\tlog.Panicf(\"%s is invalid webhook type\", os.Getenv(EnvModeWebhookType))\n\t\t}\n\n\t\tif mwt == ModeWebhookTypeCheckRun {\n\t\t\tlog.Println(\"WARNING: check_run is deprecated mode and will delete it. Please use workflow_job\")\n\t\t}\n\n\t\tc.ModeWebhookType = mwt\n\t}\n\n\tc.ProvideDockerHubMetrics = false\n\tif os.Getenv(EnvProvideDockerHubMetrics) == \"true\" {\n\t\tc.ProvideDockerHubMetrics = true\n\t}\n\n\tc.DockerHubCredential = DockerHubCredential{}\n\tif c.ProvideDockerHubMetrics {\n\t\tif os.Getenv(EnvDockerHubUsername) != \"\" && os.Getenv(EnvDockerHubPassword) != \"\" {\n\t\t\tc.DockerHubCredential.Username = os.Getenv(EnvDockerHubUsername)\n\t\t\tc.DockerHubCredential.Password = os.Getenv(EnvDockerHubPassword)\n\t\t} else {\n\t\t\tlog.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\")\n\t\t}\n\t} else {\n\t\tlog.Println(\"Docker Hub metrics is disabled\")\n\t}\n\n\tc.MaxConnectionsToBackend = 50\n\tif os.Getenv(EnvMaxConnectionsToBackend) != \"\" {\n\t\tnumberPB, err := strconv.ParseInt(os.Getenv(EnvMaxConnectionsToBackend), 10, 64)\n\t\tif err != nil {\n\t\t\tlog.Panicf(\"failed to convert int64 %s: %+v\", EnvMaxConnectionsToBackend, err)\n\t\t}\n\t\tc.MaxConnectionsToBackend = numberPB\n\t}\n\tc.MaxConcurrencyDeleting = 1\n\tif os.Getenv(EnvMaxConcurrencyDeleting) != \"\" {\n\t\tnumberCD, err := strconv.ParseInt(os.Getenv(EnvMaxConcurrencyDeleting), 10, 64)\n\t\tif err != nil {\n\t\t\tlog.Panicf(\"failed to convert int64 %s: %+v\", EnvMaxConcurrencyDeleting, err)\n\t\t}\n\t\tc.MaxConcurrencyDeleting = numberCD\n\t}\n\n\tc.GitHubURL = \"https://github.com\"\n\tif os.Getenv(EnvGitHubURL) != \"\" {\n\t\tu, err := url.Parse(os.Getenv(EnvGitHubURL))\n\t\tif err != nil {\n\t\t\tlog.Panicf(\"failed to parse URL %s: %+v\", os.Getenv(EnvGitHubURL), err)\n\t\t}\n\n\t\tif strings.EqualFold(u.Scheme, \"\") {\n\t\t\tlog.Panicf(\"%s must has scheme (value: %s)\", EnvGitHubURL, os.Getenv(EnvGitHubURL))\n\t\t}\n\t\tif strings.EqualFold(u.Host, \"\") {\n\t\t\tlog.Panicf(\"%s must has host (value: %s)\", EnvGitHubURL, os.Getenv(EnvGitHubURL))\n\t\t}\n\n\t\tc.GitHubURL = os.Getenv(EnvGitHubURL)\n\t}\n\n\tif os.Getenv(EnvRunnerVersion) == \"\" {\n\t\tc.RunnerVersion = \"latest\"\n\t} else {\n\t\t// valid value: \"latest\" or \"vX.XXX.X\"\n\t\tswitch os.Getenv(EnvRunnerVersion) {\n\t\tcase \"latest\":\n\t\t\tc.RunnerVersion = \"latest\"\n\t\tdefault:\n\t\t\t_, err := version.NewVersion(os.Getenv(EnvRunnerVersion))\n\t\t\tif err != nil {\n\t\t\t\tlog.Panicf(\"failed to parse input runner version: %+v\", err)\n\t\t\t}\n\n\t\t\tc.RunnerVersion = os.Getenv(EnvRunnerVersion)\n\t\t}\n\t}\n\n\tc.ShoesPluginOutputPath = \".\"\n\tif os.Getenv(EnvShoesPluginOutputPath) != \"\" {\n\t\tc.ShoesPluginOutputPath = os.Getenv(EnvShoesPluginOutputPath)\n\t}\n\n\tConfig = c\n\treturn c\n}\n\n// LoadGitHubApps load config for GitHub Apps\nfunc LoadGitHubApps() *GitHubApp {\n\tvar ga GitHubApp\n\tappID, err := strconv.ParseInt(os.Getenv(EnvGitHubAppID), 10, 64)\n\tif err != nil {\n\t\tlog.Panicf(\"failed to parse %s: %+v\", EnvGitHubAppID, err)\n\t}\n\tga.AppID = appID\n\n\tpemBase64ed := os.Getenv(EnvGitHubAppPrivateKeyBase64)\n\tif pemBase64ed == \"\" {\n\t\tlog.Panicf(\"%s must be set\", EnvGitHubAppPrivateKeyBase64)\n\t}\n\tpemByte, err := base64.StdEncoding.DecodeString(pemBase64ed)\n\tif err != nil {\n\t\tlog.Panicf(\"failed to decode base64 %s: %+v\", EnvGitHubAppPrivateKeyBase64, err)\n\t}\n\tga.PEMByte = pemByte\n\n\tblock, _ := pem.Decode(pemByte)\n\tif block == nil {\n\t\tlog.Panicf(\"%s is invalid format, please input private key \", EnvGitHubAppPrivateKeyBase64)\n\t}\n\tkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tif err != nil {\n\t\tlog.Panicf(\"%s is invalid format, failed to parse private key: %+v\", EnvGitHubAppPrivateKeyBase64, err)\n\t}\n\tga.PEM = key\n\n\tappSecret := os.Getenv(EnvGitHubAppSecret)\n\tif appSecret == \"\" {\n\t\tlog.Panicf(\"%s must be set\", EnvGitHubAppSecret)\n\t}\n\tga.AppSecret = []byte(appSecret)\n\n\treturn &ga\n}\n\n// LoadMySQLURL load MySQL URL from environment\nfunc LoadMySQLURL() string {\n\tmysqlHost, ok_Host := os.LookupEnv(EnvMySQLHost)\n\tmysqlPort, ok_Port := os.LookupEnv(EnvMySQLPort)\n\tmysqlUser, ok_User := os.LookupEnv(EnvMySQLUser)\n\tmysqlPassword, ok_Password := os.LookupEnv(EnvMySQLPassword)\n\tmysqlDatabase, ok_Database := os.LookupEnv(EnvMySQLDatabase)\n\tif ok_Host && ok_Port && ok_User && ok_Password && ok_Database {\n\t\tmysqlURL := fmt.Sprintf(\"%s:%s@tcp(%s:%s)/%s\", mysqlUser, mysqlPassword, mysqlHost, mysqlPort, mysqlDatabase)\n\t\tlog.Println(\"load MySQL URL from environment variables MYSQL_USER, MYSQL_PASSWORD, MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, not MYSQL_URL\")\n\t\treturn mysqlURL\n\t}\n\tmysqlURL := os.Getenv(EnvMySQLURL)\n\tif mysqlURL == \"\" {\n\t\tlog.Panicf(\"%s must be set\", EnvMySQLURL)\n\t}\n\treturn mysqlURL\n}\n\n// LoadPluginPath load plugin path from environment\nfunc LoadPluginPath() string {\n\tpluginPath := os.Getenv(EnvShoesPluginPath)\n\tif pluginPath == \"\" {\n\t\tlog.Panicf(\"%s must be set\", EnvShoesPluginPath)\n\t}\n\tfp, err := fetch(pluginPath)\n\tif err != nil {\n\t\tlog.Panicf(\"failed to fetch plugin binary: %+v\", err)\n\t}\n\tabsPath, err := checkBinary(fp)\n\tif err != nil {\n\t\tlog.Panicf(\"failed to check plugin binary: %+v\", err)\n\t}\n\tlog.Printf(\"use plugin path is %s\\n\", absPath)\n\treturn absPath\n}\n\nfunc checkBinary(p string) (string, error) {\n\tf, err := os.ReadFile(p)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to open file: %w\", err)\n\t}\n\n\t// check binary type\n\tmineType := http.DetectContentType(f)\n\tif !strings.EqualFold(mineType, \"application/octet-stream\") {\n\t\treturn \"\", fmt.Errorf(\"invalid file type (correct: application/octet-stream got: %s)\", mineType)\n\t}\n\n\t// need permission of execute\n\tif err := os.Chmod(p, 0777); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to chmod: %w\", err)\n\t}\n\n\tif filepath.IsAbs(p) {\n\t\treturn p, nil\n\t}\n\n\tapath, err := filepath.Abs(p)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get abs: %w\", err)\n\t}\n\n\treturn apath, nil\n}\n\n// fetch retrieve plugin binaries.\n// return saved file path.\nfunc fetch(p string) (string, error) {\n\t_, err := os.Stat(p)\n\tif err == nil {\n\t\t// this is file path!\n\t\treturn p, nil\n\t}\n\n\tu, err := url.Parse(p)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse input url: %w\", err)\n\t}\n\tswitch u.Scheme {\n\tcase \"http\", \"https\":\n\t\treturn fetchHTTP(u)\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unsupported fetch schema (scheme: %s)\", u.Scheme)\n\t}\n}\n\n// fetchHTTP fetch plugin binary over HTTP(s).\n// save to current directory.\nfunc fetchHTTP(u *url.URL) (string, error) {\n\tlog.Printf(\"fetch plugin binary from %s\\n\", u.String())\n\tdir := Config.ShoesPluginOutputPath\n\tif strings.EqualFold(dir, \".\") {\n\t\tpwd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to working directory: %w\", err)\n\t\t}\n\t\tdir = pwd\n\t}\n\n\tp := strings.Split(u.Path, \"/\")\n\tfileName := p[len(p)-1]\n\n\tfp := filepath.Join(dir, fileName)\n\tf, err := os.Create(fp)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create os file: %w\", err)\n\t}\n\tdefer f.Close()\n\n\tresp, err := http.Get(u.String())\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get config via HTTP(S): %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"failed to get config via HTTP(S): status code is not 200 (status code: %d)\", resp.StatusCode)\n\t}\n\n\tif _, err := io.Copy(f, resp.Body); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to write file (path: %s): %w\", fp, err)\n\t}\n\n\treturn fp, nil\n}\n"
  },
  {
    "path": "pkg/datastore/github.go",
    "content": "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/myshoes/pkg/logger\"\n\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n)\n\n// NewClientInstallationByRepo create a client of GitHub using installation ID from repo name\nfunc NewClientInstallationByRepo(ctx context.Context, ds Datastore, repo string) (*github.Client, *Target, error) {\n\ttarget, err := SearchRepo(ctx, ds, repo)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to search repository: %w\", err)\n\t}\n\n\tinstallationID, err := gh.IsInstalledGitHubApp(ctx, target.Scope)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get installation ID: %w\", err)\n\t}\n\n\tclient, err := gh.NewClientInstallation(installationID)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to create client: %w\", err)\n\t}\n\n\treturn client, target, nil\n}\n\n// PendingWorkflowRunWithTarget is struct for pending workflow run\ntype PendingWorkflowRunWithTarget struct {\n\tTarget      *Target\n\tWorkflowRun *github.WorkflowRun\n}\n\n// GetPendingWorkflowRunByRecentRepositories get pending workflow runs by recent active repositories\nfunc GetPendingWorkflowRunByRecentRepositories(ctx context.Context, ds Datastore) ([]PendingWorkflowRunWithTarget, error) {\n\tpendingRuns, err := getPendingWorkflowRunByRecentRepositories(ctx, ds)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get pending workflow runs: %w\", err)\n\t}\n\n\tqueuedJob, err := ds.ListJobs(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get list of jobs: %w\", err)\n\t}\n\n\tvar result []PendingWorkflowRunWithTarget\n\t// We ignore the pending run if the job is already queued.\n\tfor _, pendingRun := range pendingRuns {\n\t\tfound := false\n\t\tfor _, job := range queuedJob {\n\t\t\twebhookEvent, err := github.ParseWebHook(\"workflow_job\", []byte(job.CheckEventJSON))\n\t\t\tif err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to parse webhook payload (job id: %s): %+v\", job.UUID, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tworkflowJob, ok := webhookEvent.(*github.WorkflowJobEvent)\n\t\t\tif !ok {\n\t\t\t\tlogger.Logf(false, \"failed to cast to WorkflowJobEvent (job id: %s)\", job.UUID)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif pendingRun.WorkflowRun.GetID() == workflowJob.GetWorkflowJob().GetRunID() {\n\t\t\t\tlogger.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())\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\tresult = append(result, pendingRun)\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc getPendingWorkflowRunByRecentRepositories(ctx context.Context, ds Datastore) ([]PendingWorkflowRunWithTarget, error) {\n\trecentActiveRepositories, err := getRecentRepositories(ctx, ds)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get recent repositories: %w\", err)\n\t}\n\n\tvar pendingRuns []PendingWorkflowRunWithTarget\n\tvar wg sync.WaitGroup\n\tvar mu sync.Mutex\n\tfor _, repoRawURL := range recentActiveRepositories {\n\t\twg.Add(1)\n\t\tgo func(repoRawURL string) {\n\t\t\tdefer wg.Done()\n\t\t\tu, err := url.Parse(repoRawURL)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to get pending run by recent repositories: failed to parse repository url: %+v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfullName := strings.TrimPrefix(u.Path, \"/\")\n\t\t\tclient, target, err := NewClientInstallationByRepo(ctx, ds, fullName)\n\t\t\tif err != nil {\n\t\t\t\tlogger.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)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\towner, repo := gh.DivideScope(fullName)\n\t\t\tpendingRunsByRepo, err := getPendingRunByRepo(ctx, client, owner, repo)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to get pending run by recent repositories: failed to get pending run by repo (full_name: %s) %+v\", fullName, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmu.Lock()\n\t\t\tfor _, run := range pendingRunsByRepo {\n\t\t\t\tpendingRuns = append(pendingRuns, PendingWorkflowRunWithTarget{\n\t\t\t\t\tTarget:      target,\n\t\t\t\t\tWorkflowRun: run,\n\t\t\t\t})\n\t\t\t}\n\t\t\tmu.Unlock()\n\t\t}(repoRawURL)\n\t}\n\n\twg.Wait()\n\n\treturn pendingRuns, nil\n}\n\nfunc getPendingRunByRepo(ctx context.Context, client *github.Client, owner, repo string) ([]*github.WorkflowRun, error) {\n\truns, err := gh.ListWorkflowRunsNewest(ctx, client, owner, repo, 50)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list runs: %w\", err)\n\t}\n\n\tvar pendingRuns []*github.WorkflowRun\n\tfor _, r := range runs {\n\t\tif r.GetStatus() == \"queued\" || r.GetStatus() == \"pending\" {\n\t\t\toldMinutes := 10\n\t\t\tsinceMinutes := time.Since(r.CreatedAt.Time).Minutes()\n\t\t\tif sinceMinutes >= float64(oldMinutes) {\n\t\t\t\tlogger.Logf(false, \"workflow run %d is pending over %d minutes, So will enqueue (repo: %s/%s)\", r.GetID(), oldMinutes, owner, repo)\n\t\t\t\tpendingRuns = append(pendingRuns, r)\n\t\t\t} else {\n\t\t\t\tlogger.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)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pendingRuns, nil\n}\n\nfunc getRecentRepositories(ctx context.Context, ds Datastore) ([]string, error) {\n\trecent := time.Now().Add(-1 * time.Hour)\n\trecentRunners, err := ds.ListRunnersLogBySince(ctx, recent)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get targets from datastore: %w\", err)\n\t}\n\n\t// sort by created_at\n\tsort.SliceStable(recentRunners, func(i, j int) bool {\n\t\treturn recentRunners[i].CreatedAt.After(recentRunners[j].CreatedAt)\n\t})\n\n\t// unique repositories\n\trecentActiveRepositories := make(map[string]struct{})\n\tfor _, r := range recentRunners {\n\t\tu := r.RepositoryURL\n\t\tif _, ok := recentActiveRepositories[u]; !ok {\n\t\t\trecentActiveRepositories[u] = struct{}{}\n\t\t}\n\t}\n\tvar result []string\n\tfor repository := range recentActiveRepositories {\n\t\tresult = append(result, repository)\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "pkg/datastore/interface.go",
    "content": "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/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\n// Error values\nvar (\n\tErrNotFound = errors.New(\"not found\")\n)\n\n// Lock values\nvar (\n\tIsLocked    = \"is locked\"\n\tIsNotLocked = \"is not locked\"\n)\n\n// Datastore is persistent storage\ntype Datastore interface {\n\tCreateTarget(ctx context.Context, target Target) error\n\tGetTarget(ctx context.Context, id uuid.UUID) (*Target, error)\n\tGetTargetByScope(ctx context.Context, scope string) (*Target, error)\n\tListTargets(ctx context.Context) ([]Target, error)\n\tDeleteTarget(ctx context.Context, id uuid.UUID) error\n\n\t// Deprecated: Use datastore.UpdateTargetStatus.\n\tUpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus TargetStatus, description string) error\n\tUpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error\n\n\tUpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType ResourceType, newProviderURL sql.NullString) error\n\n\tEnqueueJob(ctx context.Context, job Job) error\n\tListJobs(ctx context.Context) ([]Job, error)\n\tDeleteJob(ctx context.Context, id uuid.UUID) error\n\n\tCreateRunner(ctx context.Context, runner Runner) error\n\tListRunners(ctx context.Context) ([]Runner, error)\n\tListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]Runner, error)\n\tListRunnersLogBySince(ctx context.Context, since time.Time) ([]Runner, error)\n\tGetRunner(ctx context.Context, id uuid.UUID) (*Runner, error)\n\tDeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason RunnerStatus) error\n\n\t// Lock\n\tGetLock(ctx context.Context) error\n\tIsLocked(ctx context.Context) (string, error)\n}\n\n// Target is a target repository that will add auto-scaling runner.\ntype Target struct {\n\tUUID  uuid.UUID `db:\"uuid\" json:\"id\"`\n\tScope string    `db:\"scope\" json:\"scope\"` // repo (:owner/:repo) or org (:organization)\n\t// deprecated\n\tGitHubToken    string         `db:\"github_token\" json:\"github_token\"`\n\tTokenExpiredAt time.Time      `db:\"token_expired_at\" json:\"token_expired_at\"`\n\tGHEDomain      sql.NullString `db:\"ghe_domain\" json:\"ghe_domain\"`\n\n\tResourceType      ResourceType   `db:\"resource_type\" json:\"resource_type\"`\n\tProviderURL       sql.NullString `db:\"provider_url\" json:\"provider_url\"`\n\tStatus            TargetStatus   `db:\"status\" json:\"status\"`\n\tStatusDescription sql.NullString `db:\"status_description\" json:\"status_description\"`\n\tCreatedAt         time.Time      `db:\"created_at\" json:\"created_at\"`\n\tUpdatedAt         time.Time      `db:\"updated_at\" json:\"updated_at\"`\n}\n\n// OwnerRepo return :owner and :repo\nfunc (t *Target) OwnerRepo() (string, string) {\n\treturn gh.DivideScope(t.Scope)\n}\n\n// CanReceiveJob check status in target\nfunc (t *Target) CanReceiveJob() bool {\n\tswitch t.Status {\n\tcase TargetStatusSuspend, TargetStatusDeleted:\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// ListTargets get list of target that can receive job\nfunc ListTargets(ctx context.Context, ds Datastore) ([]Target, error) {\n\ttargets, err := ds.ListTargets(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get targets from datastore: %w\", err)\n\t}\n\n\tvar result []Target\n\n\tfor _, t := range targets {\n\t\tif t.CanReceiveJob() {\n\t\t\tresult = append(result, t)\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// UpdateTargetStatus update datastore\nfunc UpdateTargetStatus(ctx context.Context, ds Datastore, targetID uuid.UUID, newStatus TargetStatus, description string) error {\n\ttarget, err := ds.GetTarget(ctx, targetID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get target: %w\", err)\n\t}\n\n\tif !target.CanReceiveJob() {\n\t\t// not change status\n\t\treturn nil\n\t}\n\n\tif err := ds.UpdateTargetStatus(ctx, targetID, newStatus, description); err != nil {\n\t\tlogger.Logf(false, \"failed to update target status: %+v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// SearchRepo search datastore.Target from datastore\n// format of repo is \"orgs/repos\"\nfunc SearchRepo(ctx context.Context, ds Datastore, repo string) (*Target, error) {\n\tsep := strings.Split(repo, \"/\")\n\tif len(sep) != 2 {\n\t\treturn nil, fmt.Errorf(\"incorrect repo format ex: orgs/repo (input: %s)\", repo)\n\t}\n\n\t// use repo scope if set repo\n\trepoTarget, err := ds.GetTargetByScope(ctx, repo)\n\tif err == nil && repoTarget.CanReceiveJob() {\n\t\treturn repoTarget, nil\n\t} else if err != nil && !errors.Is(err, ErrNotFound) {\n\t\treturn nil, fmt.Errorf(\"failed to get target from repo: %w\", err)\n\t}\n\n\t// repo is not found, so search org target\n\torg := sep[0]\n\torgTarget, err := ds.GetTargetByScope(ctx, org)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get target from organization: %w\", err)\n\t}\n\n\tif !orgTarget.CanReceiveJob() {\n\t\treturn nil, fmt.Errorf(\"target is not active\")\n\t}\n\n\treturn orgTarget, nil\n}\n\n// TargetStatus is status for target\ntype TargetStatus string\n\n// TargetStatus variables\nconst (\n\tTargetStatusActive  TargetStatus = \"active\" //lint:ignore SA9004 this is status\n\tTargetStatusRunning              = \"running\"\n\tTargetStatusSuspend              = \"suspend\"\n\tTargetStatusDeleted              = \"deleted\"\n\tTargetStatusErr                  = \"error\"\n)\n\n// Job is a runner job\ntype Job struct {\n\tUUID           uuid.UUID      `db:\"uuid\"`\n\tGHEDomain      sql.NullString `db:\"ghe_domain\"`\n\tRepository     string         `db:\"repository\"` // repo (:owner/:repo)\n\tCheckEventJSON string         `db:\"check_event\"`\n\tTargetID       uuid.UUID      `db:\"target_id\"`\n\tCreatedAt      time.Time      `db:\"created_at\" json:\"created_at\"`\n\tUpdatedAt      time.Time      `db:\"updated_at\" json:\"updated_at\"`\n}\n\n// RepoURL return repository URL that send webhook.\nfunc (j *Job) RepoURL() string {\n\tserverURL := \"https://github.com\"\n\tif j.GHEDomain.Valid {\n\t\tserverURL = j.GHEDomain.String\n\t}\n\n\ts := strings.Split(serverURL, \"://\")\n\n\tvar u url.URL\n\tu.Scheme = s[0]\n\tu.Host = s[1]\n\tu.Path = j.Repository\n\n\treturn u.String()\n}\n\n// Runner is a runner\ntype Runner struct {\n\tUUID           uuid.UUID      `db:\"runner_id\"`\n\tShoesType      string         `db:\"shoes_type\"`\n\tIPAddress      string         `db:\"ip_address\"`\n\tTargetID       uuid.UUID      `db:\"target_id\"`\n\tCloudID        string         `db:\"cloud_id\"`\n\tDeleted        bool           `db:\"deleted\"`\n\tStatus         RunnerStatus   `db:\"status\"`\n\tResourceType   ResourceType   `db:\"resource_type\"`\n\tRunnerUser     sql.NullString `db:\"runner_user\" json:\"runner_user\"`\n\tProviderURL    sql.NullString `db:\"provider_url\" json:\"provider_url\"`\n\tRepositoryURL  string         `db:\"repository_url\"`\n\tRequestWebhook string         `db:\"request_webhook\"`\n\tCreatedAt      time.Time      `db:\"created_at\"`\n\tUpdatedAt      time.Time      `db:\"updated_at\"`\n\tDeletedAt      sql.NullTime   `db:\"deleted_at\"`\n}\n\n// RunnerStatus is status for runner\ntype RunnerStatus string\n\n// RunnerStatus variables\nconst (\n\tRunnerStatusCreated        RunnerStatus = \"created\" //lint:ignore SA9004 this is status\n\tRunnerStatusCompleted                   = \"completed\"\n\tRunnerStatusReachHardLimit              = \"reach_hard_limit\"\n)\n"
  },
  {
    "path": "pkg/datastore/memory/memory.go",
    "content": "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.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// Memory is implement datastore on-memory\ntype Memory struct {\n\tmu      *sync.RWMutex\n\ttargets map[uuid.UUID]datastore.Target\n\tjobs    map[uuid.UUID]datastore.Job\n\trunners map[uuid.UUID]datastore.Runner\n}\n\n// New create map\nfunc New() (*Memory, error) {\n\tm := &sync.RWMutex{}\n\tt := map[uuid.UUID]datastore.Target{}\n\tj := map[uuid.UUID]datastore.Job{}\n\tr := map[uuid.UUID]datastore.Runner{}\n\n\treturn &Memory{\n\t\tmu:      m,\n\t\ttargets: t,\n\t\tjobs:    j,\n\t\trunners: r,\n\t}, nil\n}\n\n// CreateTarget create a target\nfunc (m *Memory) CreateTarget(ctx context.Context, target datastore.Target) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tm.targets[target.UUID] = target\n\treturn nil\n}\n\n// GetTarget get a target\nfunc (m *Memory) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tt, ok := m.targets[id]\n\tif !ok {\n\t\treturn nil, datastore.ErrNotFound\n\t}\n\treturn &t, nil\n}\n\n// GetTargetByScope get a target from scope\nfunc (m *Memory) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tfor _, t := range m.targets {\n\t\tif t.Scope == scope {\n\t\t\t// found\n\t\t\treturn &t, nil\n\n\t\t}\n\t}\n\n\treturn nil, datastore.ErrNotFound\n}\n\n// ListTargets get a all targets\nfunc (m *Memory) ListTargets(ctx context.Context) ([]datastore.Target, error) {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tvar targets []datastore.Target\n\n\tfor _, t := range m.targets {\n\t\ttargets = append(targets, t)\n\t}\n\n\treturn targets, nil\n}\n\n// DeleteTarget delete a target\nfunc (m *Memory) DeleteTarget(ctx context.Context, id uuid.UUID) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tdelete(m.targets, id)\n\treturn nil\n}\n\n// UpdateTargetStatus update status in target\nfunc (m *Memory) UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus datastore.TargetStatus, description string) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tt, ok := m.targets[targetID]\n\tif !ok {\n\t\treturn fmt.Errorf(\"not found\")\n\t}\n\n\tt.Status = newStatus\n\tif description != \"\" {\n\t\tt.StatusDescription.Valid = true\n\t} else {\n\t\tt.StatusDescription.Valid = false\n\t}\n\tt.StatusDescription.String = description\n\n\tm.targets[targetID] = t\n\n\treturn nil\n}\n\n// UpdateToken update token in target\nfunc (m *Memory) UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tt, ok := m.targets[targetID]\n\tif !ok {\n\t\treturn fmt.Errorf(\"not found\")\n\t}\n\tt.GitHubToken = newToken\n\tt.TokenExpiredAt = newExpiredAt\n\n\tm.targets[targetID] = t\n\treturn nil\n}\n\n// UpdateTargetParam update parameter of target\nfunc (m *Memory) UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType datastore.ResourceType, newProviderURL string) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tt, ok := m.targets[targetID]\n\tif !ok {\n\t\treturn fmt.Errorf(\"not found\")\n\t}\n\tt.ResourceType = newResourceType\n\tt.ProviderURL = sql.NullString{\n\t\tString: newProviderURL,\n\t\tValid:  true,\n\t}\n\n\tm.targets[targetID] = t\n\treturn nil\n}\n\n// EnqueueJob add a job\nfunc (m *Memory) EnqueueJob(ctx context.Context, job datastore.Job) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tm.jobs[job.UUID] = job\n\treturn nil\n}\n\n// ListJobs get all jobs\nfunc (m *Memory) ListJobs(ctx context.Context) ([]datastore.Job, error) {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tvar jobs []datastore.Job\n\tfor _, j := range m.jobs {\n\t\tjobs = append(jobs, j)\n\t}\n\n\treturn jobs, nil\n}\n\n// DeleteJob delete a job\nfunc (m *Memory) DeleteJob(ctx context.Context, id uuid.UUID) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tdelete(m.jobs, id)\n\treturn nil\n}\n\n// CreateRunner add a runner\nfunc (m *Memory) CreateRunner(ctx context.Context, runner datastore.Runner) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tm.runners[runner.UUID] = runner\n\n\treturn nil\n}\n\n// ListRunners get a all runners\nfunc (m *Memory) ListRunners(ctx context.Context) ([]datastore.Runner, error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tvar runners []datastore.Runner\n\tfor _, r := range m.runners {\n\t\trunners = append(runners, r)\n\t}\n\n\treturn runners, nil\n}\n\n// ListRunnersByTargetID get a not deleted runners that has target_id\nfunc (m *Memory) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tvar runners []datastore.Runner\n\tfor _, r := range m.runners {\n\t\tif uuid.Equal(r.TargetID, targetID) {\n\t\t\trunners = append(runners, r)\n\t\t}\n\t}\n\n\treturn runners, nil\n}\n\n// ListRunnersLogBySince ListRunnerLog get a runners since time\nfunc (m *Memory) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tvar runners []datastore.Runner\n\tfor _, r := range m.runners {\n\t\tif r.CreatedAt.After(since) {\n\t\t\trunners = append(runners, r)\n\t\t}\n\t}\n\n\treturn runners, nil\n}\n\n// GetRunner get a runner\nfunc (m *Memory) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tr, ok := m.runners[id]\n\tif !ok {\n\t\treturn nil, datastore.ErrNotFound\n\t}\n\n\treturn &r, nil\n}\n\n// DeleteRunner delete a runner\nfunc (m *Memory) DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason datastore.RunnerStatus) error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tdelete(m.runners, id)\n\treturn nil\n}\n\n// GetLock get lock\nfunc (m *Memory) GetLock(ctx context.Context) error {\n\treturn nil\n}\n\n// IsLocked return status of lock\nfunc (m *Memory) IsLocked(ctx context.Context) (string, error) {\n\treturn datastore.IsNotLocked, nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/job.go",
    "content": "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/whywaita/myshoes/pkg/datastore\"\n)\n\n// EnqueueJob add a job\nfunc (m *MySQL) EnqueueJob(ctx context.Context, job datastore.Job) error {\n\tquery := `INSERT INTO jobs(uuid, ghe_domain, repository, check_event, target_id) VALUES (?, ?, ?, ?, ?)`\n\tif _, err := m.Conn.ExecContext(ctx, query, job.UUID, job.GHEDomain, job.Repository, job.CheckEventJSON, job.TargetID.String()); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute INSERT query: %w\", err)\n\t}\n\n\tselect {\n\tcase m.notifyEnqueueCh <- struct{}{}:\n\t\t// notified to starter\n\tdefault:\n\t\t// no capacity on channel, do not block\n\t}\n\n\treturn nil\n}\n\n// ListJobs get all jobs\nfunc (m *MySQL) ListJobs(ctx context.Context) ([]datastore.Job, error) {\n\tvar jobs []datastore.Job\n\tquery := `SELECT uuid, ghe_domain, repository, check_event, target_id, created_at, updated_at FROM jobs`\n\tif err := m.Conn.SelectContext(ctx, &jobs, query); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn jobs, nil\n}\n\n// DeleteJob delete a job\nfunc (m *MySQL) DeleteJob(ctx context.Context, id uuid.UUID) error {\n\tquery := `DELETE FROM jobs WHERE uuid = ?`\n\tif _, err := m.Conn.ExecContext(ctx, query, id.String()); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute DELETE query: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/job_test.go",
    "content": "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/cmp\"\n\t\"github.com/jmoiron/sqlx\"\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\nvar testJobID = uuid.FromStringOrNil(\"1b4e5b7a-e3c1-4829-9cfd-eac4183f2c95\")\n\nfunc TestMySQL_EnqueueJob(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:  testTargetID,\n\t\tScope: testScopeRepo,\n\t\tGHEDomain: sql.NullString{\n\t\t\tValid: false,\n\t\t},\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput datastore.Job\n\t\twant  *datastore.Job\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: datastore.Job{\n\t\t\t\tUUID:           testJobID,\n\t\t\t\tRepository:     testScopeRepo,\n\t\t\t\tCheckEventJSON: `{\"example\": \"json\"}`,\n\t\t\t\tTargetID:       testTargetID,\n\t\t\t},\n\t\t\twant: &datastore.Job{\n\t\t\t\tUUID:           testJobID,\n\t\t\t\tRepository:     testScopeRepo,\n\t\t\t\tCheckEventJSON: `{\"example\": \"json\"}`,\n\t\t\t\tTargetID:       testTargetID,\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\terr := testDatastore.EnqueueJob(context.Background(), test.input)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to enqueue job: %+v\", err)\n\t\t}\n\t\tgot, err := getJobFromSQL(testDB, test.input.UUID)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get job from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_ListJobs(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:  testTargetID,\n\t\tScope: testScopeRepo,\n\t\tGHEDomain: sql.NullString{\n\t\t\tValid: false,\n\t\t},\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput []datastore.Job\n\t\twant  []datastore.Job\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: []datastore.Job{\n\t\t\t\t{\n\t\t\t\t\tUUID:           testJobID,\n\t\t\t\t\tRepository:     testScopeRepo,\n\t\t\t\t\tCheckEventJSON: `{\"example\": \"json\"}`,\n\t\t\t\t\tTargetID:       testTargetID,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []datastore.Job{\n\t\t\t\t{\n\t\t\t\t\tUUID:           testJobID,\n\t\t\t\t\tRepository:     testScopeRepo,\n\t\t\t\t\tCheckEventJSON: `{\"example\": \"json\"}`,\n\t\t\t\t\tTargetID:       testTargetID,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tfor _, input := range test.input {\n\t\t\terr := testDatastore.EnqueueJob(context.Background(), input)\n\t\t\tif !test.err && err != nil {\n\t\t\t\tt.Fatalf(\"failed to enqueue job: %+v\", err)\n\t\t\t}\n\t\t}\n\n\t\tgot, err := testDatastore.ListJobs(context.Background())\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get jobs: %+v\", err)\n\t\t}\n\t\tif len(test.want) != len(got) {\n\t\t\tt.Fatalf(\"incorrect length jobs, want: %d but got: %d\", len(test.want), len(got))\n\t\t}\n\t\tfor i := range got {\n\t\t\tgot[i].CreatedAt = time.Time{}\n\t\t\tgot[i].UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_DeleteJob(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:  testTargetID,\n\t\tScope: testScopeRepo,\n\t\tGHEDomain: sql.NullString{\n\t\t\tValid: false,\n\t\t},\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\tif err := testDatastore.EnqueueJob(context.Background(), datastore.Job{\n\t\tUUID:           testJobID,\n\t\tRepository:     testScopeRepo,\n\t\tCheckEventJSON: `{\"example\": \"json\"}`,\n\t\tTargetID:       testTargetID,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to enqueue job: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *datastore.Job\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: testJobID,\n\t\t\twant:  nil,\n\t\t\terr:   false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\terr := testDatastore.DeleteJob(context.Background(), test.input)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to delete job: %+v\", err)\n\t\t}\n\n\t\tgot, err := getJobFromSQL(testDB, test.input)\n\t\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\t\tt.Fatalf(\"failed to get job from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc getJobFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Job, error) {\n\tvar j datastore.Job\n\tquery := `SELECT uuid, ghe_domain, repository, check_event, target_id FROM jobs WHERE uuid = ?`\n\tstmt, err := testDB.Preparex(query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare: %w\", err)\n\t}\n\terr = stmt.Get(&j, id)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get job: %w\", err)\n\t}\n\treturn &j, nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/lock.go",
    "content": "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\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// GetLock get lock\nfunc (m *MySQL) GetLock(ctx context.Context) error {\n\tvar res int\n\n\tcfg, err := mysql.ParseDSN(config.Config.MySQLDSN)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse DSN: %w\", err)\n\t}\n\tlockKey := cfg.DBName\n\n\tquery := fmt.Sprintf(`SELECT GET_LOCK('%s', 10)`, lockKey)\n\tif err := m.Conn.GetContext(ctx, &res, query); err != nil {\n\t\treturn fmt.Errorf(\"failed to GET_LOCK: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// IsLocked return status of lock\nfunc (m *MySQL) IsLocked(ctx context.Context) (string, error) {\n\tvar res int\n\n\tcfg, err := mysql.ParseDSN(config.Config.MySQLDSN)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse DSN: %w\", err)\n\t}\n\tlockKey := cfg.DBName\n\n\tquery := fmt.Sprintf(`SELECT IS_FREE_LOCK('%s')`, lockKey)\n\tif err := m.Conn.GetContext(ctx, &res, query); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to IS_FREE_LOCK: %w\", err)\n\t}\n\n\tswitch res {\n\tcase 1:\n\t\treturn datastore.IsNotLocked, nil\n\tcase 0:\n\t\treturn datastore.IsLocked, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"IS_FREE_LOCK return NULL\")\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/mysql.go",
    "content": "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 implement datastore in MySQL\ntype MySQL struct {\n\tConn *sqlx.DB\n\n\tnotifyEnqueueCh chan<- struct{}\n}\n\n// New create mysql connection\nfunc New(dsn string, notifyEnqueueCh chan<- struct{}) (*MySQL, error) {\n\tu, err := getMySQLURL(dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get MySQL URL: %w\", err)\n\t}\n\n\tconn, err := sqlx.Open(\"mysql\", u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create mysql connection: %w\", err)\n\t}\n\n\treturn &MySQL{\n\t\tConn:            conn,\n\t\tnotifyEnqueueCh: notifyEnqueueCh,\n\t}, nil\n}\n\nfunc getMySQLURL(dsn string) (string, error) {\n\tc, err := mysql.ParseDSN(dsn)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse DSN: %w\", err)\n\t}\n\n\tc.Loc = time.UTC\n\tc.ParseTime = true\n\tc.Collation = \"utf8mb4_general_ci\"\n\tif c.Params == nil {\n\t\tc.Params = map[string]string{}\n\t}\n\tc.Params[\"sql_mode\"] = \"'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'\"\n\n\tc.InterpolateParams = true\n\n\treturn c.FormatDSN(), nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/mysql_test.go",
    "content": "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 *testing.M) {\n\tos.Exit(testutils.IntegrationTestRunner(m))\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/runner.go",
    "content": "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.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// CreateRunner add a runner\nfunc (m *MySQL) CreateRunner(ctx context.Context, runner datastore.Runner) error {\n\ttx := m.Conn.MustBegin()\n\n\tqueryRunner := `INSERT INTO runners(uuid) VALUES (?)`\n\tif _, err := tx.ExecContext(ctx, queryRunner, runner.UUID.String()); err != nil {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute INSERT query runners: %w\", err)\n\t}\n\n\tqueryDetail := `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`\n\tif _, 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 {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute INSERT query runner_detail: %w\", err)\n\t}\n\n\tqueryRunning := `INSERT INTO runners_running(runner_id) VALUES (?)`\n\tif _, err := tx.ExecContext(ctx, queryRunning, runner.UUID.String()); err != nil {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute INSERT query runners_running: %w\", err)\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute COMMIT: %w\", err)\n\t}\n\treturn nil\n}\n\n// ListRunners get a not deleted runners\nfunc (m *MySQL) ListRunners(ctx context.Context) ([]datastore.Runner, error) {\n\tvar runners []datastore.Runner\n\tquery := `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\n FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id`\n\terr := m.Conn.SelectContext(ctx, &runners, query)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn runners, nil\n}\n\n// ListRunnersByTargetID get a not deleted runners that has target_id\nfunc (m *MySQL) ListRunnersByTargetID(ctx context.Context, targetID uuid.UUID) ([]datastore.Runner, error) {\n\tvar runners []datastore.Runner\n\tquery := `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\n FROM runners_running AS runner JOIN runner_detail AS detail ON runner.runner_id = detail.runner_id WHERE detail.target_id = ?`\n\terr := m.Conn.SelectContext(ctx, &runners, query, targetID)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn runners, nil\n}\n\n// ListRunnersLogBySince ListRunnerLog get a runners since time\nfunc (m *MySQL) ListRunnersLogBySince(ctx context.Context, since time.Time) ([]datastore.Runner, error) {\n\tvar runners []datastore.Runner\n\n\tquery := `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 > ?`\n\terr := m.Conn.SelectContext(ctx, &runners, query, since)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn runners, nil\n}\n\n// GetRunner get a runner\nfunc (m *MySQL) GetRunner(ctx context.Context, id uuid.UUID) (*datastore.Runner, error) {\n\tvar r datastore.Runner\n\n\tquery := `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 = ?`\n\tif err := m.Conn.GetContext(ctx, &r, query, id.String()); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn &r, nil\n}\n\n// DeleteRunner delete a runner\nfunc (m *MySQL) DeleteRunner(ctx context.Context, id uuid.UUID, deletedAt time.Time, reason datastore.RunnerStatus) error {\n\ttx := m.Conn.MustBegin()\n\n\tqueryDelete := `DELETE FROM runners_running WHERE runner_id = ?`\n\tif _, err := tx.ExecContext(ctx, queryDelete, id.String()); err != nil {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute DELETE query: %w\", err)\n\t}\n\n\tqueryInsert := `INSERT INTO runners_deleted(runner_id, reason) VALUES (?, ?)`\n\tif _, err := tx.ExecContext(ctx, queryInsert, id.String(), reason); err != nil {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute INSERT query: %w\", err)\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\ttx.Rollback()\n\t\treturn fmt.Errorf(\"failed to execute COMMIT: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/runner_test.go",
    "content": "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/cmp\"\n\t\"github.com/jmoiron/sqlx\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\nvar testRunnerID = uuid.FromStringOrNil(\"7943e412-c0ae-4068-ab24-3e71a13fbe53\")\n\nfunc TestMySQL_CreateRunner(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\ttests := []struct {\n\t\tinput datastore.Runner\n\t\twant  *datastore.Runner\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: datastore.Runner{\n\t\t\t\tUUID:           testRunnerID,\n\t\t\t\tShoesType:      \"shoes-test\",\n\t\t\t\tTargetID:       testTargetID,\n\t\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t},\n\t\t\twant: &datastore.Runner{\n\t\t\t\tUUID:           testRunnerID,\n\t\t\t\tShoesType:      \"shoes-test\",\n\t\t\t\tTargetID:       testTargetID,\n\t\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\tinput: datastore.Runner{\n\t\t\t\tUUID:         testRunnerID,\n\t\t\t\tShoesType:    \"shoes-test\",\n\t\t\t\tTargetID:     testTargetID,\n\t\t\t\tCloudID:      \"mycloud-uuid\",\n\t\t\t\tResourceType: datastore.ResourceTypeNano,\n\t\t\t\tRunnerUser: sql.NullString{\n\t\t\t\t\tString: \"runner\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: \"./shoes-test\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t},\n\t\t\twant: &datastore.Runner{\n\t\t\t\tUUID:         testRunnerID,\n\t\t\t\tShoesType:    \"shoes-test\",\n\t\t\t\tTargetID:     testTargetID,\n\t\t\t\tCloudID:      \"mycloud-uuid\",\n\t\t\t\tResourceType: datastore.ResourceTypeNano,\n\t\t\t\tRunnerUser: sql.NullString{\n\t\t\t\t\tString: \"runner\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: \"./shoes-test\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\tUUID:           testTargetID,\n\t\t\tScope:          testScopeRepo,\n\t\t\tGitHubToken:    testGitHubToken,\n\t\t\tTokenExpiredAt: testTime,\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t}); err != nil {\n\t\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t\t}\n\n\t\terr := testDatastore.CreateRunner(context.Background(), test.input)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to create runner: %+v\", err)\n\t\t}\n\t\tgot, err := getRunnerFromSQL(testDB, test.input.UUID)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get runner from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tteardown()\n\t}\n}\n\nfunc TestMySQL_ListRunners(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput []datastore.Runner\n\t\twant  []datastore.Runner\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: []datastore.Runner{\n\t\t\t\t{\n\t\t\t\t\tUUID:           testRunnerID,\n\t\t\t\t\tShoesType:      \"shoes-test\",\n\t\t\t\t\tTargetID:       testTargetID,\n\t\t\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []datastore.Runner{\n\t\t\t\t{\n\t\t\t\t\tUUID:           testRunnerID,\n\t\t\t\t\tShoesType:      \"shoes-test\",\n\t\t\t\t\tTargetID:       testTargetID,\n\t\t\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tfor _, input := range test.input {\n\t\t\terr := testDatastore.CreateRunner(context.Background(), input)\n\t\t\tif !test.err && err != nil {\n\t\t\t\tt.Fatalf(\"failed to create runner: %+v\", err)\n\t\t\t}\n\t\t}\n\n\t\tgot, err := testDatastore.ListRunners(context.Background())\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get runners: %+v\", err)\n\t\t}\n\t\tif len(test.want) != len(got) {\n\t\t\tt.Fatalf(\"incorrect length runners, want: %d but got: %d\", len(test.want), len(got))\n\t\t}\n\t\tfor i := range got {\n\t\t\tgot[i].CreatedAt = time.Time{}\n\t\t\tgot[i].UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_ListRunnersNotReturnDeleted(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\tu := \"00000000-0000-0000-0000-00000000000%d\"\n\n\tfor i := 0; i < 3; i++ {\n\t\tinput := datastore.Runner{\n\t\t\tUUID:           testRunnerID,\n\t\t\tShoesType:      \"shoes-test\",\n\t\t\tTargetID:       testTargetID,\n\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\tRequestWebhook: \"{}\",\n\t\t}\n\t\tinput.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))\n\t\terr := testDatastore.CreateRunner(context.Background(), input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create runner: %+v\", err)\n\t\t}\n\t}\n\n\terr := testDatastore.DeleteRunner(context.Background(), uuid.FromStringOrNil(fmt.Sprintf(u, 0)), time.Now(), \"deleted\")\n\tif err != nil {\n\t\tt.Fatalf(\"failed to delete runner: %+v\", err)\n\t}\n\n\tgot, err := testDatastore.ListRunners(context.Background())\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get runners: %+v\", err)\n\t}\n\tfor i := range got {\n\t\tgot[i].CreatedAt = time.Time{}\n\t\tgot[i].UpdatedAt = time.Time{}\n\t}\n\n\tvar want []datastore.Runner\n\tfor i := 1; i < 3; i++ {\n\t\tr := datastore.Runner{\n\t\t\tUUID:           testRunnerID,\n\t\t\tShoesType:      \"shoes-test\",\n\t\t\tTargetID:       testTargetID,\n\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\tRequestWebhook: \"{}\",\n\t\t}\n\t\tr.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))\n\t\twant = append(want, r)\n\t}\n\n\tif diff := cmp.Diff(want, got); diff != \"\" {\n\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestMySQL_ListRunnersLogBySince(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\tu := \"00000000-0000-0000-0000-00000000000%d\"\n\n\tfor i := 1; i < 3; i++ {\n\t\tinput := datastore.Runner{\n\t\t\tUUID:           testRunnerID,\n\t\t\tShoesType:      \"shoes-test\",\n\t\t\tTargetID:       testTargetID,\n\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\tRequestWebhook: \"{}\",\n\t\t}\n\t\tinput.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))\n\t\terr := testDatastore.CreateRunner(context.Background(), input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create runner: %+v\", err)\n\t\t}\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}\n\n\trecent := time.Now().Add(-10 * time.Second)\n\tgot, err := testDatastore.ListRunnersLogBySince(context.Background(), recent)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get runners: %+v\", err)\n\t}\n\tfor i := range got {\n\t\tgot[i].CreatedAt = time.Time{}\n\t\tgot[i].UpdatedAt = time.Time{}\n\t}\n\n\tvar want []datastore.Runner\n\tfor i := 1; i < 3; i++ {\n\t\tr := datastore.Runner{\n\t\t\tUUID:           testRunnerID,\n\t\t\tShoesType:      \"shoes-test\",\n\t\t\tTargetID:       testTargetID,\n\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\tRequestWebhook: \"{}\",\n\t\t}\n\t\tr.UUID = uuid.FromStringOrNil(fmt.Sprintf(u, i))\n\t\twant = append(want, r)\n\t}\n\n\tif diff := cmp.Diff(want, got); diff != \"\" {\n\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc TestMySQL_GetRunner(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\tif err := testDatastore.CreateRunner(context.Background(), datastore.Runner{\n\t\tUUID:           testRunnerID,\n\t\tShoesType:      \"shoes-test\",\n\t\tTargetID:       testTargetID,\n\t\tCloudID:        \"mycloud-uuid\",\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\tRequestWebhook: \"{}\",\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create runner: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *datastore.Runner\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: testRunnerID,\n\t\t\twant: &datastore.Runner{\n\t\t\t\tUUID:           testRunnerID,\n\t\t\t\tShoesType:      \"shoes-test\",\n\t\t\t\tTargetID:       testTargetID,\n\t\t\t\tCloudID:        \"mycloud-uuid\",\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\t\t\tRequestWebhook: \"{}\",\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tgot, err := testDatastore.GetRunner(context.Background(), test.input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get runner: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_DeleteRunner(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\tif err := testDatastore.CreateRunner(context.Background(), datastore.Runner{\n\t\tUUID:           testRunnerID,\n\t\tShoesType:      \"shoes-test\",\n\t\tTargetID:       testTargetID,\n\t\tCloudID:        \"mycloud-uuid\",\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\tRequestWebhook: \"{}\",\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create runner: %+v\", err)\n\t}\n\n\tdeleted := datastore.Runner{\n\t\tUUID:           testRunnerID,\n\t\tShoesType:      \"shoes-test\",\n\t\tTargetID:       testTargetID,\n\t\tCloudID:        \"mycloud-uuid\",\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t\tRepositoryURL:  \"https://github.com/octocat/Hello-World\",\n\t\tRequestWebhook: \"{}\",\n\t}\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *datastore.Runner\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: testRunnerID,\n\t\t\twant:  &deleted,\n\t\t\terr:   false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\terr := testDatastore.DeleteRunner(context.Background(), test.input, time.Now().UTC(), datastore.RunnerStatusCompleted)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t\t}\n\t\tgot, err := getRunnerFromSQL(testDB, test.input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get target from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t\tgot.DeletedAt = sql.NullTime{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tif _, err := getRunningRunnerFromSQL(testDB, test.input); err == nil || errors.Is(err, sql.ErrNoRows) {\n\t\t\tt.Errorf(\"%s is deleted, but exist in runner_running: %+v\", test.input, err)\n\t\t}\n\t\tif _, err := getDeletedRunnerFromSQL(testDB, test.input); err != nil {\n\t\t\tt.Fatalf(\"%s is not exist in runners_deleted: %+v\", test.input, err)\n\t\t}\n\t}\n}\n\nfunc getRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {\n\tvar r datastore.Runner\n\tquery := `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 = ?`\n\tstmt, err := testDB.Preparex(query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare: %w\", err)\n\t}\n\terr = stmt.Get(&r, id)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get runner: %w\", err)\n\t}\n\treturn &r, nil\n}\n\nfunc getRunningRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {\n\tvar r datastore.Runner\n\tquery := `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\nFROM runner_detail AS detail JOIN runnesr_running AS running ON detail.runner_id = running.runner_id WHERE detail.runner_id = ?`\n\tstmt, err := testDB.Preparex(query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare: %w\", err)\n\t}\n\terr = stmt.Get(&r, id)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get runner: %w\", err)\n\t}\n\treturn &r, nil\n}\n\nfunc getDeletedRunnerFromSQL(testDB *sqlx.DB, id uuid.UUID) (*datastore.Runner, error) {\n\tvar r datastore.Runner\n\tquery := `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\nFROM runner_detail AS detail JOIN runners_deleted AS deleted ON detail.runner_id = deleted.runner_id WHERE detail.runner_id = ?`\n\tstmt, err := testDB.Preparex(query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare: %w\", err)\n\t}\n\terr = stmt.Get(&r, id)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get runner: %w\", err)\n\t}\n\treturn &r, nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/schema.sql",
    "content": "CREATE TABLE `targets` (\n    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,\n    `scope` VARCHAR(255) NOT NULL,\n    `ghe_domain` VARCHAR(255),\n    `github_token` VARCHAR(255) NOT NULL,\n    `token_expired_at` TIMESTAMP NOT NULL,\n    `resource_type` ENUM('nano', 'micro', 'small', 'medium', 'large', 'xlarge', '2xlarge', '3xlarge', '4xlarge') NOT NULL,\n    `provider_url` VARCHAR(255),\n    `status` VARCHAR(255) NOT NULL DEFAULT 'active',\n    `status_description` VARCHAR(255),\n    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,\n    `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,\n    UNIQUE KEY `ghe_domain_scope` (`ghe_domain`, `scope`)\n);\n\nCREATE TABLE `runners` (\n    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,\n    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp\n);\n\nCREATE TABLE `runner_detail` (\n    `runner_id` VARCHAR(36) NOT NULL,\n    `shoes_type` VARCHAR(255) NOT NULL,\n    `ip_address` VARCHAR(255) NOT NULL,\n    `target_id` VARCHAR(36) NOT NULL,\n    `cloud_id` TEXT NOT NULL,\n    `resource_type` ENUM('nano', 'micro', 'small', 'medium', 'large', 'xlarge', '2xlarge', '3xlarge', '4xlarge') NOT NULL,\n    `runner_user` VARCHAR(255),\n    `provider_url` VARCHAR(255),\n    `repository_url` VARCHAR(255) NOT NULL,\n    `request_webhook` TEXT NOT NULL,\n    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,\n    `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,\n    KEY `fk_runner_target_id` (`target_id`),\n    CONSTRAINT `runners_ibfk_1` FOREIGN KEY fk_runner_target_id(`target_id`) REFERENCES targets(`uuid`) ON DELETE RESTRICT,\n    KEY `fk_runner_detail_id` (`runner_id`),\n    CONSTRAINT `runners_ibfk_2` FOREIGN KEY fk_runner_detail_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE RESTRICT\n);\n\nCREATE TABLE `runners_running` (\n    `runner_id` VARCHAR(36) NOT NULL,\n    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,\n    KEY `fk_runner_deleted_id` (`runner_id`),\n    CONSTRAINT `runners_running_ibfk_1` FOREIGN KEY fk_runner_deleted_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE CASCADE\n);\n\nCREATE TABLE `runners_deleted` (\n    `runner_id` VARCHAR(36) NOT NULL,\n    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,\n    `reason` VARCHAR(255) NOT NULL,\n    KEY `fk_runner_deleted_id` (`runner_id`),\n    CONSTRAINT `runners_deleted_ibfk_1` FOREIGN KEY fk_runner_deleted_id(`runner_id`) REFERENCES runners(`uuid`) ON DELETE CASCADE\n);\n\nCREATE TABLE `jobs` (\n    `uuid` VARCHAR(36) NOT NULL PRIMARY KEY,\n    `ghe_domain` VARCHAR(255),\n    `repository` VARCHAR(255) NOT NULL,\n    `check_event` TEXT NOT NULL,\n    `target_id` VARCHAR(36) NOT NULL,\n    `created_at` TIMESTAMP NOT NULL DEFAULT current_timestamp,\n    `updated_at` TIMESTAMP NOT NULL DEFAULT current_timestamp ON UPDATE current_timestamp,\n    KEY `fk_job_target_id` (`target_id`),\n    CONSTRAINT `jobs_ibfk_1` FOREIGN KEY fk_job_target_id(`target_id`) REFERENCES targets(`uuid`) ON DELETE RESTRICT\n);\n"
  },
  {
    "path": "pkg/datastore/mysql/target.go",
    "content": "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.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// CreateTarget create a target\nfunc (m *MySQL) CreateTarget(ctx context.Context, target datastore.Target) error {\n\texpiredAtRFC3339 := target.TokenExpiredAt.Format(\"2006-01-02 15:04:05\")\n\n\tquery := `INSERT INTO targets(uuid, scope, ghe_domain, github_token, token_expired_at, resource_type, provider_url) VALUES (?, ?, ?, ?, ?, ?, ?)`\n\tif _, err := m.Conn.ExecContext(\n\t\tctx,\n\t\tquery,\n\t\ttarget.UUID,\n\t\ttarget.Scope,\n\t\ttarget.GHEDomain,\n\t\ttarget.GitHubToken,\n\t\texpiredAtRFC3339,\n\t\ttarget.ResourceType,\n\t\ttarget.ProviderURL,\n\t); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute INSERT query: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// GetTarget get a target\nfunc (m *MySQL) GetTarget(ctx context.Context, id uuid.UUID) (*datastore.Target, error) {\n\tvar t datastore.Target\n\tquery := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets WHERE uuid = ?`\n\tif err := m.Conn.GetContext(ctx, &t, query, id.String()); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn &t, nil\n}\n\n// GetTargetByScope get a target from scope\nfunc (m *MySQL) GetTargetByScope(ctx context.Context, scope string) (*datastore.Target, error) {\n\tvar t datastore.Target\n\tquery := 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)\n\tif err := m.Conn.GetContext(ctx, &t, query); err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, datastore.ErrNotFound\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to execute SELECT query: %w\", err)\n\t}\n\n\treturn &t, nil\n}\n\n// ListTargets get a all target\nfunc (m *MySQL) ListTargets(ctx context.Context) ([]datastore.Target, error) {\n\tvar ts []datastore.Target\n\tquery := `SELECT uuid, scope, github_token, token_expired_at, resource_type, provider_url, status, status_description, created_at, updated_at FROM targets`\n\tif err := m.Conn.SelectContext(ctx, &ts, query); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to SELECT query: %w\", err)\n\t}\n\n\treturn ts, nil\n}\n\n// DeleteTarget delete a target\nfunc (m *MySQL) DeleteTarget(ctx context.Context, id uuid.UUID) error {\n\tquery := `UPDATE targets SET status = \"deleted\" WHERE uuid = ?`\n\tif _, err := m.Conn.ExecContext(ctx, query, id.String()); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute DELETE query: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// UpdateTargetStatus update status in target\nfunc (m *MySQL) UpdateTargetStatus(ctx context.Context, targetID uuid.UUID, newStatus datastore.TargetStatus, description string) error {\n\tquery := `UPDATE targets SET status = ?, status_description = ? WHERE uuid = ?`\n\tif _, err := m.Conn.ExecContext(ctx, query, newStatus, description, targetID.String()); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute UPDATE query: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// UpdateToken update token in target\nfunc (m *MySQL) UpdateToken(ctx context.Context, targetID uuid.UUID, newToken string, newExpiredAt time.Time) error {\n\tquery := `UPDATE targets SET github_token = ?, token_expired_at = ? WHERE uuid = ?`\n\tif _, err := m.Conn.ExecContext(ctx, query, newToken, newExpiredAt, targetID.String()); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute UPDATE query: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// UpdateTargetParam update parameter of target\nfunc (m *MySQL) UpdateTargetParam(ctx context.Context, targetID uuid.UUID, newResourceType datastore.ResourceType, newProviderURL sql.NullString) error {\n\tquery := `UPDATE targets SET resource_type = ?, provider_url = ? WHERE uuid = ?`\n\tif _, err := m.Conn.ExecContext(ctx, query, newResourceType, newProviderURL, targetID.String()); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute UPDATE query: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/datastore/mysql/target_test.go",
    "content": "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/cmp\"\n\t\"github.com/jmoiron/sqlx\"\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\nvar testTargetID = uuid.FromStringOrNil(\"8a72d42c-372c-4e0d-9c6a-4304d44af137\")\nvar testTargetID2 = uuid.FromStringOrNil(\"d14ccfea-b123-4ada-974e-bbff0937e9c7\")\nvar testScopeOrg = \"octocat\"\nvar testScopeRepo = \"octocat/hello-world\"\nvar testScopeRepo2 = \"octocat/hello-world2\"\nvar testGitHubToken = \"this-code-is-github-token\"\nvar testRunnerUser = \"testing-super-user\"\nvar testProviderURL = \"/shoes-mock\"\nvar testTime = time.Date(2037, 9, 3, 0, 0, 0, 0, time.UTC)\n\nfunc TestMySQL_CreateTarget(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\ttests := []struct {\n\t\tinput datastore.Target\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: datastore.Target{\n\t\t\t\tUUID:           testTargetID,\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID,\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\tinput: datastore.Target{\n\t\t\t\tUUID:           testTargetID2,\n\t\t\t\tScope:          testScopeRepo2,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tGHEDomain: sql.NullString{\n\t\t\t\t\tString: \"https://example.com\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tResourceType: datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID2,\n\t\t\t\tScope:          testScopeRepo2,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tGHEDomain: sql.NullString{\n\t\t\t\t\tString: \"https://example.com\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tStatus:       datastore.TargetStatusActive,\n\t\t\t\tResourceType: datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\terr := testDatastore.CreateTarget(context.Background(), test.input)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t\t}\n\t\tgot, err := getTargetFromSQL(testDB, test.input.UUID)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get target from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_GetTarget(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\terr := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t\tProviderURL: sql.NullString{\n\t\t\tString: testProviderURL,\n\t\t\tValid:  true,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: testTargetID,\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID,\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tgot, err := testDatastore.GetTarget(context.Background(), test.input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get target: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_GetTargetByScope(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\ttests := []struct {\n\t\tinput   string\n\t\twant    *datastore.Target\n\t\tprepare func() error\n\t\terr     bool\n\t}{\n\t\t{\n\t\t\t// create single instance\n\t\t\tinput: testScopeRepo,\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID,\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tprepare: func() error {\n\t\t\t\treturn testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\t\t\tUUID:           testTargetID,\n\t\t\t\t\tScope:          testScopeRepo,\n\t\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\t\tValid:  true,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\t// repository is active and organization is deleted, correct return repository\n\t\t\tinput: testScopeRepo,\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID,\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tprepare: func() error {\n\t\t\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\t\t\tUUID:           testTargetID,\n\t\t\t\t\tScope:          testScopeRepo,\n\t\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\t\tValid:  true,\n\t\t\t\t\t},\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create repository: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\t\t\tUUID:           testTargetID2,\n\t\t\t\t\tScope:          testScopeOrg,\n\t\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\t\tValid:  true,\n\t\t\t\t\t},\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create organization (will delete): %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif err := testDatastore.DeleteTarget(context.Background(), testTargetID2); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to delete organization: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\t// repository is deleted and organization is active, correct return organization\n\t\t\tinput: testScopeOrg,\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID2,\n\t\t\t\tScope:          testScopeOrg,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tprepare: func() error {\n\t\t\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\t\t\tUUID:           testTargetID,\n\t\t\t\t\tScope:          testScopeRepo,\n\t\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\t\tValid:  true,\n\t\t\t\t\t},\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create repository (will delete): %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif err := testDatastore.DeleteTarget(context.Background(), testTargetID); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to delete repository: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\t\t\tUUID:           testTargetID2,\n\t\t\t\t\tScope:          testScopeOrg,\n\t\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\t\tValid:  true,\n\t\t\t\t\t},\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to create deleted organization: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tif err := test.prepare(); err != nil {\n\t\t\tt.Fatalf(\"failed to prepare function: %+v\", err)\n\t\t}\n\n\t\tgot, err := testDatastore.GetTargetByScope(context.Background(), test.input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get target: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tteardown()\n\t}\n}\n\nfunc TestMySQL_ListTargets(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t\tProviderURL: sql.NullString{\n\t\t\tString: testProviderURL,\n\t\t\tValid:  true,\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput interface{}\n\t\twant  []datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: nil,\n\t\t\twant: []datastore.Target{\n\t\t\t\t{\n\t\t\t\t\tUUID:           testTargetID,\n\t\t\t\t\tScope:          testScopeRepo,\n\t\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\t\tValid:  true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tgot, err := testDatastore.ListTargets(context.Background())\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to list targets: %+v\", err)\n\t\t}\n\t\tfor i := range got {\n\t\t\tgot[i].CreatedAt = time.Time{}\n\t\t\tgot[i].UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_DeleteTarget(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\tUUID:           testTargetID,\n\t\tScope:          testScopeRepo,\n\t\tGitHubToken:    testGitHubToken,\n\t\tTokenExpiredAt: testTime,\n\t\tResourceType:   datastore.ResourceTypeNano,\n\t\tProviderURL: sql.NullString{\n\t\t\tString: testProviderURL,\n\t\t\tValid:  true,\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t}\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: testTargetID,\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           testTargetID,\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusDeleted,\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\terr := testDatastore.DeleteTarget(context.Background(), test.input)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to delete target: %+v\", err)\n\t\t}\n\t\tgot, err := getTargetFromSQL(testDB, test.input)\n\t\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\t\tt.Fatalf(\"failed to get target from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_UpdateStatus(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\ttype Input struct {\n\t\tstatus      datastore.TargetStatus\n\t\tdescription string\n\t}\n\n\ttests := []struct {\n\t\tinput Input\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: Input{\n\t\t\t\tstatus:      datastore.TargetStatusActive,\n\t\t\t\tdescription: \"\",\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusActive,\n\t\t\t\tStatusDescription: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\tinput: Input{\n\t\t\t\tstatus:      datastore.TargetStatusRunning,\n\t\t\t\tdescription: \"job-id\",\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    testGitHubToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusRunning,\n\t\t\t\tStatusDescription: sql.NullString{\n\t\t\t\t\tString: \"job-id\",\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttID := uuid.NewV4()\n\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\tUUID:           tID,\n\t\t\tScope:          testScopeRepo,\n\t\t\tGitHubToken:    testGitHubToken,\n\t\t\tTokenExpiredAt: testTime,\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tProviderURL: sql.NullString{\n\t\t\t\tString: testProviderURL,\n\t\t\t\tValid:  true,\n\t\t\t},\n\t\t}); err != nil {\n\t\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t\t}\n\n\t\t//lint:ignore SA1019 only execute in test\n\t\terr := testDatastore.UpdateTargetStatus(context.Background(), tID, test.input.status, test.input.description)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to update status: %+v\", err)\n\t\t}\n\t\tgot, err := getTargetFromSQL(testDB, tID)\n\t\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\t\tt.Fatalf(\"failed to get target from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.UUID = uuid.UUID{}\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tif err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {\n\t\t\tt.Fatalf(\"failed to delete target: %+v\", err)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_UpdateToken(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\ttype Input struct {\n\t\ttoken   string\n\t\texpired time.Time\n\t}\n\n\ttests := []struct {\n\t\tinput Input\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: Input{\n\t\t\t\ttoken:   \"new-token\",\n\t\t\t\texpired: testTime.Add(1 * time.Hour),\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tScope:          testScopeRepo,\n\t\t\t\tGitHubToken:    \"new-token\",\n\t\t\t\tTokenExpiredAt: testTime.Add(1 * time.Hour),\n\t\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusActive,\n\t\t\t\tStatusDescription: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttID := uuid.NewV4()\n\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\tUUID:           tID,\n\t\t\tScope:          testScopeRepo,\n\t\t\tGitHubToken:    testGitHubToken,\n\t\t\tTokenExpiredAt: testTime,\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tProviderURL: sql.NullString{\n\t\t\t\tString: testProviderURL,\n\t\t\t\tValid:  true,\n\t\t\t},\n\t\t}); err != nil {\n\t\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t\t}\n\n\t\terr := testDatastore.UpdateToken(context.Background(), tID, test.input.token, test.input.expired)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to update status: %+v\", err)\n\t\t}\n\t\tgot, err := getTargetFromSQL(testDB, tID)\n\t\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\t\tt.Fatalf(\"failed to get target from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.UUID = uuid.UUID{}\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tif err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {\n\t\t\tt.Fatalf(\"failed to delete target: %+v\", err)\n\t\t}\n\t}\n}\n\nfunc TestMySQL_UpdateTargetParam(t *testing.T) {\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\ttestDB, _ := testutils.GetTestDB()\n\n\ttype input struct {\n\t\tresourceType datastore.ResourceType\n\t\trunnerUser   sql.NullString\n\t\tproviderURL  sql.NullString\n\t}\n\n\ttests := []struct {\n\t\tinput input\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: input{\n\t\t\t\tresourceType: datastore.ResourceTypeLarge,\n\t\t\t\trunnerUser: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t\tproviderURL: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tScope:        testScopeRepo,\n\t\t\t\tGitHubToken:  testGitHubToken,\n\t\t\t\tResourceType: datastore.ResourceTypeLarge,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusActive,\n\t\t\t\tStatusDescription: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\tinput: input{\n\t\t\t\tresourceType: datastore.ResourceTypeLarge,\n\t\t\t\trunnerUser: sql.NullString{\n\t\t\t\t\tString: testRunnerUser,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tproviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tScope:        testScopeRepo,\n\t\t\t\tGitHubToken:  testGitHubToken,\n\t\t\t\tResourceType: datastore.ResourceTypeLarge,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: testProviderURL,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusActive,\n\t\t\t\tStatusDescription: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\tinput: input{\n\t\t\t\tresourceType: datastore.ResourceTypeLarge,\n\t\t\t\trunnerUser: sql.NullString{\n\t\t\t\t\tString: testRunnerUser,\n\t\t\t\t\tValid:  true,\n\t\t\t\t},\n\t\t\t\tproviderURL: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &datastore.Target{\n\t\t\t\tScope:        testScopeRepo,\n\t\t\t\tGitHubToken:  testGitHubToken,\n\t\t\t\tResourceType: datastore.ResourceTypeLarge,\n\t\t\t\tProviderURL: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t\tStatus: datastore.TargetStatusActive,\n\t\t\t\tStatusDescription: sql.NullString{\n\t\t\t\t\tString: \"\",\n\t\t\t\t\tValid:  false,\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttID := uuid.NewV4()\n\t\tif err := testDatastore.CreateTarget(context.Background(), datastore.Target{\n\t\t\tUUID:           tID,\n\t\t\tScope:          testScopeRepo,\n\t\t\tGitHubToken:    testGitHubToken,\n\t\t\tTokenExpiredAt: testTime,\n\t\t\tResourceType:   datastore.ResourceTypeNano,\n\t\t\tProviderURL: sql.NullString{\n\t\t\t\tString: \"test-default-string\",\n\t\t\t\tValid:  true,\n\t\t\t},\n\t\t}); err != nil {\n\t\t\tt.Fatalf(\"failed to create target: %+v\", err)\n\t\t}\n\n\t\tif err := testDatastore.UpdateTargetParam(context.Background(), tID, test.input.resourceType, test.input.providerURL); err != nil {\n\t\t\tt.Fatalf(\"failed to UpdateResourceTyoe: %+v\", err)\n\t\t}\n\n\t\tgot, err := getTargetFromSQL(testDB, tID)\n\t\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\t\tt.Fatalf(\"failed to get target from SQL: %+v\", err)\n\t\t}\n\t\tif got != nil {\n\t\t\tgot.UUID = uuid.UUID{}\n\t\t\tgot.CreatedAt = time.Time{}\n\t\t\tgot.UpdatedAt = time.Time{}\n\t\t\tgot.TokenExpiredAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tif err := testDatastore.DeleteTarget(context.Background(), tID); err != nil {\n\t\t\tt.Fatalf(\"failed to delete target: %+v\", err)\n\t\t}\n\t}\n}\n\nfunc getTargetFromSQL(testDB *sqlx.DB, uuid uuid.UUID) (*datastore.Target, error) {\n\tvar t datastore.Target\n\tquery := `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 = ?`\n\tstmt, err := testDB.Preparex(query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare: %w\", err)\n\t}\n\terr = stmt.Get(&t, uuid)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get target: %w\", err)\n\t}\n\treturn &t, nil\n}\n"
  },
  {
    "path": "pkg/datastore/resource_type.go",
    "content": "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.go\"\n)\n\n// ResourceType is runner machine spec\ntype ResourceType int\n\n// ResourceTypes variables\nconst (\n\tResourceTypeUnknown ResourceType = iota\n\tResourceTypeNano\n\tResourceTypeMicro\n\tResourceTypeSmall\n\tResourceTypeMedium\n\tResourceTypeLarge\n\tResourceTypeXLarge\n\tResourceType2XLarge\n\tResourceType3XLarge\n\tResourceType4XLarge\n)\n\n// String implement interface for fmt.Stringer\nfunc (r ResourceType) String() string {\n\tswitch r {\n\tcase ResourceTypeNano:\n\t\treturn \"nano\"\n\tcase ResourceTypeMicro:\n\t\treturn \"micro\"\n\tcase ResourceTypeSmall:\n\t\treturn \"small\"\n\tcase ResourceTypeMedium:\n\t\treturn \"medium\"\n\tcase ResourceTypeLarge:\n\t\treturn \"large\"\n\tcase ResourceTypeXLarge:\n\t\treturn \"xlarge\"\n\tcase ResourceType2XLarge:\n\t\treturn \"2xlarge\"\n\tcase ResourceType3XLarge:\n\t\treturn \"3xlarge\"\n\tcase ResourceType4XLarge:\n\t\treturn \"4xlarge\"\n\t}\n\n\treturn \"unknown\"\n}\n\n// Value implements the database/sql/driver Valuer interface\nfunc (r ResourceType) Value() (driver.Value, error) {\n\treturn driver.Value(r.String()), nil\n}\n\n// Scan implements the database/sql Scanner interface\nfunc (r *ResourceType) Scan(src interface{}) error {\n\tvar rt *ResourceType\n\tswitch src := src.(type) {\n\tcase string:\n\t\tunmarshaled := UnmarshalResourceType(src)\n\t\trt = &unmarshaled\n\tcase []uint8:\n\t\tstr := string(src)\n\t\tunmarshaled := UnmarshalResourceType(str)\n\t\trt = &unmarshaled\n\tdefault:\n\t\treturn fmt.Errorf(\"incompatible type for ResourceType: %T\", src)\n\t}\n\n\t*r = *rt\n\treturn nil\n}\n\n// UnmarshalResourceType cast type to ResourceType\nfunc UnmarshalResourceType(src interface{}) ResourceType {\n\tswitch src := src.(type) {\n\tcase string:\n\t\treturn UnmarshalResourceTypeString(src)\n\tcase pb.ResourceType:\n\t\treturn UnmarshalResourceTypePb(src)\n\t}\n\n\treturn ResourceTypeUnknown\n}\n\n// UnmarshalResourceTypeString cast type from string to ResourceType\nfunc UnmarshalResourceTypeString(in string) ResourceType {\n\tswitch in {\n\tcase \"nano\":\n\t\treturn ResourceTypeNano\n\tcase \"micro\":\n\t\treturn ResourceTypeMicro\n\tcase \"small\":\n\t\treturn ResourceTypeSmall\n\tcase \"medium\":\n\t\treturn ResourceTypeMedium\n\tcase \"large\":\n\t\treturn ResourceTypeLarge\n\tcase \"xlarge\":\n\t\treturn ResourceTypeXLarge\n\tcase \"2xlarge\":\n\t\treturn ResourceType2XLarge\n\tcase \"3xlarge\":\n\t\treturn ResourceType3XLarge\n\tcase \"4xlarge\":\n\t\treturn ResourceType4XLarge\n\t}\n\n\treturn ResourceTypeUnknown\n}\n\n// UnmarshalResourceTypePb cast type from pb.ResourceType to ResourceType\nfunc UnmarshalResourceTypePb(in pb.ResourceType) ResourceType {\n\tswitch in {\n\tcase pb.ResourceType_Nano:\n\t\treturn ResourceTypeNano\n\tcase pb.ResourceType_Micro:\n\t\treturn ResourceTypeMicro\n\tcase pb.ResourceType_Small:\n\t\treturn ResourceTypeSmall\n\tcase pb.ResourceType_Medium:\n\t\treturn ResourceTypeMedium\n\tcase pb.ResourceType_Large:\n\t\treturn ResourceTypeLarge\n\tcase pb.ResourceType_XLarge:\n\t\treturn ResourceTypeXLarge\n\tcase pb.ResourceType_XLarge2:\n\t\treturn ResourceType2XLarge\n\tcase pb.ResourceType_XLarge3:\n\t\treturn ResourceType3XLarge\n\tcase pb.ResourceType_XLarge4:\n\t\treturn ResourceType4XLarge\n\t}\n\n\treturn ResourceTypeUnknown\n}\n\n// ToPb convert type of protobuf\nfunc (r ResourceType) ToPb() pb.ResourceType {\n\tswitch r {\n\tcase ResourceTypeNano:\n\t\treturn pb.ResourceType_Nano\n\tcase ResourceTypeMicro:\n\t\treturn pb.ResourceType_Micro\n\tcase ResourceTypeSmall:\n\t\treturn pb.ResourceType_Small\n\tcase ResourceTypeMedium:\n\t\treturn pb.ResourceType_Medium\n\tcase ResourceTypeLarge:\n\t\treturn pb.ResourceType_Large\n\tcase ResourceTypeXLarge:\n\t\treturn pb.ResourceType_XLarge\n\tcase ResourceType2XLarge:\n\t\treturn pb.ResourceType_XLarge2\n\tcase ResourceType3XLarge:\n\t\treturn pb.ResourceType_XLarge3\n\tcase ResourceType4XLarge:\n\t\treturn pb.ResourceType_XLarge4\n\t}\n\n\treturn pb.ResourceType_Unknown\n}\n\n// MarshalJSON implements the encoding/json Marshaler interface\nfunc (r ResourceType) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(r.String())\n}\n\n// UnmarshalJSON implements the encoding/json Unmarshaler interface\nfunc (r *ResourceType) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn fmt.Errorf(\"data should be a string, but got %s\", data)\n\t}\n\n\trt := UnmarshalResourceTypeString(s)\n\t*r = rt\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/docker/ratelimit.go",
    "content": "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-jwt/jwt/v4\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n)\n\n// RateLimit is Docker Hub API rate limit\ntype RateLimit struct {\n\tLimit     int\n\tRemaining int\n}\n\ntype tokenCache struct {\n\texpire time.Time\n\ttoken  string\n}\n\nvar cacheMap = make(map[int]tokenCache, 1)\n\nfunc getToken() (string, error) {\n\turl := \"https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull\"\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create request: %w\", err)\n\t}\n\tif config.Config.DockerHubCredential.Password != \"\" && config.Config.DockerHubCredential.Username != \"\" {\n\t\treq.SetBasicAuth(config.Config.DockerHubCredential.Username, config.Config.DockerHubCredential.Password)\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"request token: %w\", err)\n\t}\n\tif cache, ok := cacheMap[0]; ok && cache.expire.After(time.Now()) {\n\t\treturn cache.token, nil\n\t}\n\tdefer resp.Body.Close()\n\tbyteArray, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"read body: %w\", err)\n\t}\n\tjsonMap := make(map[string]interface{})\n\tif err := json.Unmarshal(byteArray, &jsonMap); err != nil {\n\t\treturn \"\", fmt.Errorf(\"unmarshal json: %w\", err)\n\t}\n\ttokenString, ok := jsonMap[\"token\"].(string)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"tokenString is not string\")\n\t}\n\ttoken, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"parse token: %w\", err)\n\t}\n\texp, ok := token.Claims.(jwt.MapClaims)[\"exp\"].(float64)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"exp is not float64\")\n\t}\n\tcacheMap[0] = tokenCache{\n\t\texpire: time.Unix(int64(exp), 0),\n\t\ttoken:  tokenString,\n\t}\n\treturn tokenString, nil\n}\n\n// GetRateLimit get Docker Hub API rate limit\nfunc GetRateLimit() (RateLimit, error) {\n\ttoken, err := getToken()\n\tif err != nil {\n\t\treturn RateLimit{}, fmt.Errorf(\"get token: %w\", err)\n\t}\n\turl := \"https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest\"\n\treq, err := http.NewRequest(\"HEAD\", url, nil)\n\tif err != nil {\n\t\treturn RateLimit{}, fmt.Errorf(\"create request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn RateLimit{}, fmt.Errorf(\"get rate limit: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\tlimitHeader := resp.Header.Get(\"ratelimit-limit\")\n\tif limitHeader == \"\" {\n\t\treturn RateLimit{}, fmt.Errorf(\"not found ratelimit-limit header\")\n\t}\n\tlimit, err := strconv.Atoi(strings.Split(limitHeader, \";\")[0])\n\tif err != nil {\n\t\treturn RateLimit{}, fmt.Errorf(\"parse limit: %w\", err)\n\t}\n\tremainingHeader := resp.Header.Get(\"ratelimit-remaining\")\n\tif remainingHeader == \"\" {\n\t\treturn RateLimit{}, fmt.Errorf(\"not found ratelimit-remaining header\")\n\n\t}\n\tremaining, err := strconv.Atoi(strings.Split(remainingHeader, \";\")[0])\n\tif err != nil {\n\t\treturn RateLimit{}, fmt.Errorf(\"parse remaining: %w\", err)\n\t}\n\n\treturn RateLimit{\n\t\tLimit:     limit,\n\t\tRemaining: remaining,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/gh/github.go",
    "content": "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\"\n\t\"github.com/google/go-github/v80/github\"\n\t\"github.com/m4ns0ur/httpcache\"\n\t\"github.com/patrickmn/go-cache\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"golang.org/x/oauth2\"\n)\n\nvar (\n\t// ErrNotFound is error for not found\n\tErrNotFound = fmt.Errorf(\"not found\")\n\n\t// ResponseCache is cache variable\n\tresponseCache = cache.New(5*time.Minute, 10*time.Minute)\n\n\t// rateLimitRemain is remaining of Rate limit, for metrics\n\trateLimitRemain = sync.Map{}\n\t// rateLimitLimit is limit of Rate limit, for metrics\n\trateLimitLimit = sync.Map{}\n\n\t// httpCache is shareable response cache\n\thttpCache = httpcache.NewMemoryCache()\n\t// appTransport is transport for GitHub Apps\n\tappTransport = ghinstallation.AppsTransport{}\n\t// installationTransports is map of ghinstallation.Transport for cache token of installation.\n\t// key: installationID, value: ghinstallation.Transport\n\tinstallationTransports = sync.Map{}\n)\n\n// InitializeCache create a cache\nfunc InitializeCache(appID int64, appPEM []byte) error {\n\ttr := httpcache.NewTransport(httpCache)\n\titr, err := ghinstallation.NewAppsTransport(tr, appID, appPEM)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create Apps transport: %w\", err)\n\t}\n\tappTransport = *itr\n\treturn nil\n}\n\n// NewClient create a client of GitHub\nfunc NewClient(token string) (*github.Client, error) {\n\toauth2Transport := &oauth2.Transport{\n\t\tSource: oauth2.StaticTokenSource(\n\t\t\t&oauth2.Token{AccessToken: token},\n\t\t),\n\t}\n\ttransport := &httpcache.Transport{\n\t\tTransport:           oauth2Transport,\n\t\tCache:               httpCache,\n\t\tMarkCachedResponses: true,\n\t}\n\tclientTransport := newInstrumentedTransport(transport)\n\n\tif !config.Config.IsGHES() {\n\t\treturn github.NewClient(&http.Client{Transport: clientTransport}), nil\n\t}\n\n\treturn github.NewClient(&http.Client{Transport: clientTransport}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)\n}\n\n// NewClientGitHubApps create a client of GitHub using Private Key from GitHub Apps\n// header is \"Authorization: Bearer YOUR_JWT\"\n// docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app\nfunc NewClientGitHubApps() (*github.Client, error) {\n\tif !config.Config.IsGHES() {\n\t\treturn github.NewClient(&http.Client{Transport: newInstrumentedTransport(&appTransport)}), nil\n\t}\n\n\tapiEndpoint, err := getAPIEndpoint()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get GitHub API Endpoint: %w\", err)\n\t}\n\n\titr := appTransport\n\titr.BaseURL = apiEndpoint.String()\n\treturn github.NewClient(&http.Client{Transport: newInstrumentedTransport(&appTransport)}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)\n}\n\n// NewClientInstallation create a client of GitHub using installation ID from GitHub Apps\n// header is \"Authorization: token YOUR_INSTALLATION_ACCESS_TOKEN\"\n// docs: https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-an-installation\nfunc NewClientInstallation(installationID int64) (*github.Client, error) {\n\titr := getInstallationTransport(installationID)\n\n\tif !config.Config.IsGHES() {\n\t\treturn github.NewClient(&http.Client{Transport: newInstrumentedTransport(itr)}), nil\n\t}\n\tapiEndpoint, err := getAPIEndpoint()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get GitHub API Endpoint: %w\", err)\n\t}\n\titr.BaseURL = apiEndpoint.String()\n\treturn github.NewClient(&http.Client{Transport: newInstrumentedTransport(itr)}).WithEnterpriseURLs(config.Config.GitHubURL, config.Config.GitHubURL)\n}\n\nfunc setInstallationTransport(installationID int64, itr ghinstallation.Transport) {\n\tinstallationTransports.Store(installationID, itr)\n}\n\nfunc getInstallationTransport(installationID int64) *ghinstallation.Transport {\n\tgot, found := installationTransports.Load(installationID)\n\tif !found {\n\t\treturn generateInstallationTransport(installationID)\n\t}\n\n\titr, ok := got.(ghinstallation.Transport)\n\tif !ok {\n\t\treturn generateInstallationTransport(installationID)\n\t}\n\treturn &itr\n}\n\nfunc generateInstallationTransport(installationID int64) *ghinstallation.Transport {\n\titr := ghinstallation.NewFromAppsTransport(&appTransport, installationID)\n\tsetInstallationTransport(installationID, *itr)\n\treturn itr\n}\n\n// CheckSignature check trust installation id from event.\nfunc CheckSignature(installationID int64) error {\n\tif itr := ghinstallation.NewFromAppsTransport(&appTransport, installationID); itr == nil {\n\t\treturn fmt.Errorf(\"failed to create GitHub installation\")\n\t}\n\n\treturn nil\n}\n\n// ExistRunnerReleases check exist of runner file\nfunc ExistRunnerReleases(runnerVersion string) error {\n\treleasesURL := fmt.Sprintf(\"https://github.com/actions/runner/releases/tag/%s\", runnerVersion)\n\tresp, err := http.Get(releasesURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to GET from %s: %w\", releasesURL, ErrNotFound)\n\t}\n\n\tif resp.StatusCode == http.StatusOK {\n\t\treturn nil\n\t} else if resp.StatusCode == http.StatusNotFound {\n\t\treturn ErrNotFound\n\t}\n\n\treturn fmt.Errorf(\"invalid response code (%d)\", resp.StatusCode)\n}\n\n// ExistGitHubRepository check exist of GitHub repository\nfunc ExistGitHubRepository(scope string, accessToken string) error {\n\trepoURL, err := getRepositoryURL(scope)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get repository url: %w\", err)\n\t}\n\n\tclient := &http.Client{Transport: newInstrumentedTransport(http.DefaultTransport)}\n\treq, err := http.NewRequest(http.MethodGet, repoURL, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\treq.Header.Add(\"Authorization\", fmt.Sprintf(\"token %s\", accessToken))\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to do request: %w\", err)\n\t}\n\n\tif resp.StatusCode == http.StatusOK {\n\t\treturn nil\n\t} else if resp.StatusCode == http.StatusNotFound {\n\t\treturn ErrNotFound\n\t}\n\n\treturn fmt.Errorf(\"invalid response code (%d)\", resp.StatusCode)\n}\n\nfunc getRepositoryURL(scope string) (string, error) {\n\t// github.com\n\t//   => https://api.github.com/repos/:owner/:repo\n\t//   => https://api.github.com/orgs/:owner\n\t// GitHub Enterprise Server\n\t//   => https://{your_ghe_server_url}/api/repos/:owner/:repo\n\t//   => https://{your_ghe_server_url}/api/orgs/:owner\n\n\ts := DetectScope(scope)\n\tif s == Unknown {\n\t\treturn \"\", fmt.Errorf(\"failed to detect valid scope\")\n\t}\n\n\tapiEndpoint, err := getAPIEndpoint()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get API Endpoint: %w\", err)\n\t}\n\n\tp := path.Join(apiEndpoint.Path, s.String(), scope)\n\tapiEndpoint.Path = p\n\n\treturn apiEndpoint.String(), nil\n}\n\nfunc getAPIEndpoint() (*url.URL, error) {\n\tvar apiEndpoint *url.URL\n\tif config.Config.IsGHES() {\n\t\tu, err := url.Parse(config.Config.GitHubURL)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse GHE url: %w\", err)\n\t\t}\n\n\t\tp := u.Path\n\t\tp = path.Join(p, \"api\", \"v3\")\n\t\tu.Path = p\n\t\tapiEndpoint = u\n\t} else {\n\t\tu, err := url.Parse(\"https://api.github.com\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse github.com: %w\", err)\n\t\t}\n\t\tapiEndpoint = u\n\t}\n\n\treturn apiEndpoint, nil\n}\n"
  },
  {
    "path": "pkg/gh/github_test.go",
    "content": "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) {\n\ttests := []struct {\n\t\tinput string\n\t\twant  Scope\n\t}{\n\t\t{\n\t\t\tinput: \"org/repo\",\n\t\t\twant:  Repository,\n\t\t},\n\t\t{\n\t\t\tinput: \"org\",\n\t\t\twant:  Organization,\n\t\t},\n\t\t{\n\t\t\tinput: \"org/repo/whats\",\n\t\t\twant:  Unknown,\n\t\t},\n\t\t{\n\t\t\tinput: \"https://github.com/octocat/Spoon-Knife\",\n\t\t\twant:  Unknown,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tgot := DetectScope(test.input)\n\n\t\tif got != test.want {\n\t\t\tt.Fatalf(\"want %+v, but got %+v\", test.want, got)\n\t\t}\n\t}\n}\n\ntype TestGetRepositoryURLInput struct {\n\tscope     string\n\tgheDomain string\n}\n\nfunc TestGetRepositoryURL(t *testing.T) {\n\ttests := []struct {\n\t\tinput TestGetRepositoryURLInput\n\t\twant  string\n\t\terr   error\n\t}{\n\t\t{\n\t\t\tinput: TestGetRepositoryURLInput{\n\t\t\t\tscope:     \"org/repo\",\n\t\t\t\tgheDomain: \"\",\n\t\t\t},\n\t\t\twant: \"https://api.github.com/repos/org/repo\",\n\t\t\terr:  nil,\n\t\t},\n\t\t{\n\t\t\tinput: TestGetRepositoryURLInput{\n\t\t\t\tscope:     \"org\",\n\t\t\t\tgheDomain: \"\",\n\t\t\t},\n\t\t\twant: \"https://api.github.com/orgs/org\",\n\t\t\terr:  nil,\n\t\t},\n\t\t{\n\t\t\tinput: TestGetRepositoryURLInput{\n\t\t\t\tscope:     \"org/repo\",\n\t\t\t\tgheDomain: \"https://github-enterprise.example.com\",\n\t\t\t},\n\t\t\twant: \"https://github-enterprise.example.com/api/v3/repos/org/repo\",\n\t\t\terr:  nil,\n\t\t},\n\t\t{\n\t\t\tinput: TestGetRepositoryURLInput{\n\t\t\t\tscope:     \"org\",\n\t\t\t\tgheDomain: \"https://github-enterprise.example.com\",\n\t\t\t},\n\t\t\twant: \"https://github-enterprise.example.com/api/v3/orgs/org\",\n\t\t\terr:  nil,\n\t\t},\n\t\t{\n\t\t\tinput: TestGetRepositoryURLInput{\n\t\t\t\tscope:     \"org/repo\",\n\t\t\t\tgheDomain: \"https://github-enterprise.example.com/\",\n\t\t\t},\n\t\t\twant: \"https://github-enterprise.example.com/api/v3/repos/org/repo\",\n\t\t\terr:  nil,\n\t\t},\n\t\t{\n\t\t\tinput: TestGetRepositoryURLInput{\n\t\t\t\tscope:     \"org/repo\",\n\t\t\t\tgheDomain: \"https://github-enterprise.example.com/github\",\n\t\t\t},\n\t\t\twant: \"https://github-enterprise.example.com/github/api/v3/repos/org/repo\",\n\t\t\terr:  nil,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tf := func() {\n\t\t\tif test.input.gheDomain != \"\" {\n\t\t\t\tt.Setenv(\"GITHUB_URL\", test.input.gheDomain)\n\t\t\t\tdefer os.Unsetenv(\"GITHUB_URL\")\n\t\t\t}\n\n\t\t\tconfig.LoadWithDefault()\n\n\t\t\tgot, err := getRepositoryURL(test.input.scope)\n\t\t\tif err != test.err {\n\t\t\t\tt.Fatalf(\"getRepositoryURL want err %+v, but return err %+v\", test.err, err)\n\t\t\t}\n\n\t\t\tif got != test.want {\n\t\t\t\tt.Fatalf(\"want %s, but got %s\", test.want, got)\n\t\t\t}\n\t\t}\n\t\tf()\n\t}\n}\n"
  },
  {
    "path": "pkg/gh/installation.go",
    "content": "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/pkg/logger\"\n)\n\nfunc listInstallations(ctx context.Context) ([]*github.Installation, error) {\n\tif cachedRs, found := responseCache.Get(getCacheInstallationsKey()); found {\n\t\treturn cachedRs.([]*github.Installation), nil\n\t}\n\n\tinst, err := _listInstallations(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list installations: %w\", err)\n\t}\n\n\tresponseCache.Set(getCacheInstallationsKey(), inst, 1*time.Hour)\n\n\treturn _listInstallations(ctx)\n}\n\nfunc getCacheInstallationsKey() string {\n\treturn \"installations\"\n}\n\nfunc _listInstallations(ctx context.Context) ([]*github.Installation, error) {\n\tclientApps, err := NewClientGitHubApps()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create a client Apps: %w\", err)\n\t}\n\n\tvar opts = &github.ListOptions{\n\t\tPage:    0,\n\t\tPerPage: 100,\n\t}\n\n\tvar installations []*github.Installation\n\tfor {\n\t\tlogger.Logf(true, \"get installations from GitHub, page: %d, now all installations: %d\", opts.Page, len(installations))\n\t\tis, resp, err := clientApps.Apps.ListInstallations(ctx, opts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list installations: %w\", err)\n\t\t}\n\t\tinstallations = append(installations, is...)\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\topts.Page = resp.NextPage\n\t}\n\treturn installations, nil\n}\n\nfunc listAppsInstalledRepo(ctx context.Context, installationID int64) ([]*github.Repository, error) {\n\tif cachedRs, found := responseCache.Get(getCacheInstalledRepoKey(installationID)); found {\n\t\treturn cachedRs.([]*github.Repository), nil\n\t}\n\n\tinst, err := _listAppsInstalledRepo(ctx, installationID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list installations: %w\", err)\n\t}\n\n\tresponseCache.Set(getCacheInstalledRepoKey(installationID), inst, 1*time.Hour)\n\n\treturn _listAppsInstalledRepo(ctx, installationID)\n}\n\nfunc getCacheInstalledRepoKey(installationID int64) string {\n\treturn fmt.Sprintf(\"installed-repo-%d\", installationID)\n}\n\nfunc _listAppsInstalledRepo(ctx context.Context, installationID int64) ([]*github.Repository, error) {\n\tclientInstallation, err := NewClientInstallation(installationID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create a client installation: %w\", err)\n\t}\n\n\tvar opts = &github.ListOptions{\n\t\tPage:    0,\n\t\tPerPage: 100,\n\t}\n\n\tvar repositories []*github.Repository\n\tfor {\n\t\tlogger.Logf(true, \"get list of repository from installation, page: %d, now all repositories: %d\", opts.Page, len(repositories))\n\t\tlr, resp, err := clientInstallation.Apps.ListRepos(ctx, opts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get installed repositories: %w\", err)\n\t\t}\n\t\trepositories = append(repositories, lr.Repositories...)\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\topts.Page = resp.NextPage\n\t}\n\n\treturn repositories, nil\n}\n\n// GetInstallationByID returns installation from cache by ID\nfunc GetInstallationByID(ctx context.Context, installationID int64) (*github.Installation, error) {\n\tinstallations, err := listInstallations(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get installations: %w\", err)\n\t}\n\n\tfor _, installation := range installations {\n\t\tif installation.GetID() == installationID {\n\t\t\treturn installation, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"installation not found: %d\", installationID)\n}\n\n// PurgeInstallationCache purges the cache of installations\nfunc PurgeInstallationCache(ctx context.Context) error {\n\tinstallations, err := listInstallations(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get installations: %w\", err)\n\t}\n\n\tfor _, installation := range installations {\n\t\tresponseCache.Delete(getCacheInstalledRepoKey(installation.GetID()))\n\t}\n\n\tresponseCache.Delete(getCacheInstallationsKey())\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/gh/jwt.go",
    "content": "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/google/go-github/v80/github\"\n)\n\n// function pointers (for testing)\nvar (\n\tGHlistInstallations     = listInstallations\n\tGHlistAppsInstalledRepo = listAppsInstalledRepo\n)\n\n// GenerateGitHubAppsToken generate token of GitHub Apps using private key\n// clientApps needs to response of `NewClientGitHubApps()`\nfunc GenerateGitHubAppsToken(ctx context.Context, clientApps *github.Client, installationID int64, scope string) (string, *time.Time, error) {\n\ttoken, resp, err := clientApps.Apps.CreateInstallationToken(ctx, installationID, nil)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"failed to generate token from API: %w\", err)\n\t}\n\tstoreRateLimit(scope, resp.Rate)\n\texpiresAt := token.ExpiresAt.GetTime()\n\treturn *token.Token, expiresAt, nil\n}\n\n// IsInstalledGitHubApp check installed GitHub Apps in gheDomain + inputScope\n// clientApps needs to response of `NewClientGitHubApps()`\nfunc IsInstalledGitHubApp(ctx context.Context, inputScope string) (int64, error) {\n\tinstallations, err := GHlistInstallations(ctx)\n\tif err != nil {\n\t\treturn -1, fmt.Errorf(\"failed to get list of installations: %w\", err)\n\t}\n\n\tfor _, i := range installations {\n\t\tif i.SuspendedAt != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(inputScope, *i.Account.Login) {\n\t\t\t// i.Account.Login is username or Organization name.\n\t\t\t// e.g.) `https://github.com/example/sample` -> `example/sample`\n\t\t\t// strings.HasPrefix search scope include i.Account.Login.\n\n\t\t\tswitch {\n\t\t\tcase strings.EqualFold(*i.RepositorySelection, \"all\"):\n\t\t\t\t// \"all\" can use GitHub Apps in all repositories that joined i.Account.Login.\n\t\t\t\treturn *i.ID, nil\n\t\t\tcase strings.EqualFold(*i.RepositorySelection, \"selected\"):\n\t\t\t\t// \"selected\" can use GitHub Apps in only some repositories that permitted.\n\t\t\t\t// So, need to check more using other endpoint.\n\t\t\t\terr := isInstalledGitHubAppSelected(ctx, inputScope, *i.ID)\n\t\t\t\tif err == nil {\n\t\t\t\t\t// found\n\t\t\t\t\treturn *i.ID, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn -1, &ErrIsNotInstalledGitHubApps{\n\t\tgithubURL:  config.Config.GitHubURL,\n\t\tinputScope: inputScope,\n\t}\n}\n\ntype ErrIsNotInstalledGitHubApps struct {\n\tgithubURL  string\n\tinputScope string\n}\n\nfunc (e *ErrIsNotInstalledGitHubApps) Error() string {\n\treturn fmt.Sprintf(\"%s/%s is not installed configured GitHub Apps\", e.githubURL, e.inputScope)\n}\n\nfunc (e *ErrIsNotInstalledGitHubApps) Unwrap() error {\n\treturn fmt.Errorf(\"%s\", e.Error())\n}\n\nfunc isInstalledGitHubAppSelected(ctx context.Context, inputScope string, installationID int64) error {\n\tinstalledRepository, err := GHlistAppsInstalledRepo(ctx, installationID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get list of installed repositories: %w\", err)\n\t}\n\n\tif len(installedRepository) <= 0 {\n\t\treturn fmt.Errorf(\"installed repository is not found\")\n\t}\n\n\tswitch DetectScope(inputScope) {\n\tcase Organization:\n\t\t// Scope is Organization and installed repository is existed\n\t\t// So GitHub Apps installed\n\t\treturn nil\n\tcase Repository:\n\t\tfor _, repo := range installedRepository {\n\t\t\tif strings.EqualFold(*repo.FullName, inputScope) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn fmt.Errorf(\"not found\")\n\tdefault:\n\t\treturn fmt.Errorf(\"%s can't detect scope\", inputScope)\n\t}\n}\n"
  },
  {
    "path": "pkg/gh/jwt_test.go",
    "content": "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() {\n\tGHlistInstallations = func(ctx context.Context) ([]*github.Installation, error) {\n\t\ti10 := int64(10)\n\t\ti11 := int64(11)\n\t\ti12 := int64(12)\n\t\tall := \"all\"\n\t\tselected := \"selected\"\n\t\texampleAll := \"example-all\"\n\t\texampleSelected := \"example-selected\"\n\t\texampleSuspented := \"example-suspended\"\n\n\t\treturn []*github.Installation{\n\t\t\t{\n\t\t\t\tID: &i10,\n\t\t\t\tAccount: &github.User{\n\t\t\t\t\tLogin: &exampleAll,\n\t\t\t\t},\n\t\t\t\tRepositorySelection: &all,\n\t\t\t\tSuspendedBy:         nil,\n\t\t\t},\n\t\t\t{\n\t\t\t\tID: &i11,\n\t\t\t\tAccount: &github.User{\n\t\t\t\t\tLogin: &exampleSelected,\n\t\t\t\t},\n\t\t\t\tRepositorySelection: &selected,\n\t\t\t\tSuspendedBy:         nil,\n\t\t\t},\n\t\t\t{\n\t\t\t\tID: &i12,\n\t\t\t\tAccount: &github.User{\n\t\t\t\t\tLogin: &exampleSuspented,\n\t\t\t\t},\n\t\t\t\tRepositorySelection: &selected,\n\t\t\t\tSuspendedAt: &github.Timestamp{\n\t\t\t\t\tTime: time.Now(),\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tGHlistAppsInstalledRepo = func(ctx context.Context, installationID int64) ([]*github.Repository, error) {\n\t\tfullName1 := \"example-selected/sample-registered\"\n\t\treturn []*github.Repository{\n\t\t\t{\n\t\t\t\tFullName: &fullName1,\n\t\t\t},\n\t\t}, nil\n\t}\n}\n\nfunc Test_IsInstalledGitHubApp(t *testing.T) {\n\tsetStubFunctions()\n\n\ttests := []struct {\n\t\tinput struct {\n\t\t\tgheDomain string\n\t\t\tscope     string\n\t\t}\n\t\twant int64\n\t\terr  bool\n\t}{\n\t\t{\n\t\t\tinput: struct {\n\t\t\t\tgheDomain string\n\t\t\t\tscope     string\n\t\t\t}{gheDomain: \"\", scope: \"example-all\"},\n\t\t\twant: 10,\n\t\t\terr:  false,\n\t\t},\n\t\t{\n\t\t\tinput: struct {\n\t\t\t\tgheDomain string\n\t\t\t\tscope     string\n\t\t\t}{gheDomain: \"\", scope: \"example-all/sample\"},\n\t\t\twant: 10,\n\t\t\terr:  false,\n\t\t},\n\t\t{\n\t\t\tinput: struct {\n\t\t\t\tgheDomain string\n\t\t\t\tscope     string\n\t\t\t}{gheDomain: \"\", scope: \"example-selected\"},\n\t\t\twant: 11,\n\t\t\terr:  false,\n\t\t},\n\t\t{\n\t\t\tinput: struct {\n\t\t\t\tgheDomain string\n\t\t\t\tscope     string\n\t\t\t}{gheDomain: \"\", scope: \"example-selected/sample-registered\"},\n\t\t\twant: 11,\n\t\t\terr:  false,\n\t\t},\n\t\t{\n\t\t\tinput: struct {\n\t\t\t\tgheDomain string\n\t\t\t\tscope     string\n\t\t\t}{gheDomain: \"\", scope: \"example-selected/sample-not-registered\"},\n\t\t\twant: -1,\n\t\t\terr:  true,\n\t\t},\n\t\t{\n\t\t\tinput: struct {\n\t\t\t\tgheDomain string\n\t\t\t\tscope     string\n\t\t\t}{gheDomain: \"\", scope: \"example-suspended\"},\n\t\t\twant: -1,\n\t\t\terr:  true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tgot, err := IsInstalledGitHubApp(context.Background(), test.input.scope)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to check GitHub Apps: %+v\", err)\n\t\t}\n\n\t\tif got != test.want {\n\t\t\tt.Fatalf(\"want %d, but got %d\", test.want, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/gh/label.go",
    "content": "package gh\n\nimport \"strings\"\n\n// IsRequestedMyshoesLabel checks if the job has appropriate labels for myshoes\nfunc IsRequestedMyshoesLabel(labels []string) bool {\n\t// Accept dependabot runner in GHES\n\tif len(labels) == 1 && strings.EqualFold(labels[0], \"dependabot\") {\n\t\treturn true\n\t}\n\n\tfor _, label := range labels {\n\t\tif strings.EqualFold(label, \"myshoes\") || strings.EqualFold(label, \"self-hosted\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/gh/metrics.go",
    "content": "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.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nconst githubAPINamespace = \"myshoes\"\n\nvar (\n\tgithubAPIRequestsTotal = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: githubAPINamespace,\n\t\t\tSubsystem: \"github_api\",\n\t\t\tName:      \"requests_total\",\n\t\t\tHelp:      \"Total number of GitHub API requests.\",\n\t\t},\n\t\t[]string{\"path\", \"method\", \"status_class\"},\n\t)\n\tgithubAPIRequestDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: githubAPINamespace,\n\t\t\tSubsystem: \"github_api\",\n\t\t\tName:      \"request_duration_seconds\",\n\t\t\tHelp:      \"Duration of GitHub API requests in seconds.\",\n\t\t\tBuckets:   prometheus.DefBuckets,\n\t\t},\n\t\t[]string{\"path\", \"method\", \"status_class\"},\n\t)\n\tgithubAPIErrorsTotal = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: githubAPINamespace,\n\t\t\tSubsystem: \"github_api\",\n\t\t\tName:      \"errors_total\",\n\t\t\tHelp:      \"Total number of GitHub API request errors.\",\n\t\t},\n\t\t[]string{\"path\", \"method\", \"error_type\"},\n\t)\n\tgithubAPIInflight = promauto.NewGaugeVec(\n\t\tprometheus.GaugeOpts{\n\t\t\tNamespace: githubAPINamespace,\n\t\t\tSubsystem: \"github_api\",\n\t\t\tName:      \"inflight\",\n\t\t\tHelp:      \"Number of in-flight GitHub API requests.\",\n\t\t},\n\t\t[]string{\"path\", \"method\"},\n\t)\n\tgithubAPICacheTotal = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: githubAPINamespace,\n\t\t\tSubsystem: \"github_api\",\n\t\t\tName:      \"cache_total\",\n\t\t\tHelp:      \"Total number of GitHub API cache hits/misses.\",\n\t\t},\n\t\t[]string{\"path\", \"method\", \"result\"},\n\t)\n)\n\ntype instrumentedTransport struct {\n\tnext http.RoundTripper\n}\n\nfunc newInstrumentedTransport(next http.RoundTripper) http.RoundTripper {\n\tif next == nil {\n\t\tnext = http.DefaultTransport\n\t}\n\tif _, ok := next.(*instrumentedTransport); ok {\n\t\treturn next\n\t}\n\treturn &instrumentedTransport{next: next}\n}\n\nfunc (t *instrumentedTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tstart := time.Now()\n\n\tpath := \"unknown\"\n\tmethod := \"UNKNOWN\"\n\tif req != nil {\n\t\tmethod = req.Method\n\t\tif req.URL != nil && req.URL.Path != \"\" {\n\t\t\tpath = req.URL.Path\n\t\t}\n\t}\n\n\tgithubAPIInflight.WithLabelValues(path, method).Inc()\n\tdefer githubAPIInflight.WithLabelValues(path, method).Dec()\n\n\tresp, err := t.next.RoundTrip(req)\n\n\tstatusClass := \"error\"\n\tif err == nil && resp != nil {\n\t\tstatusClass = fmt.Sprintf(\"%dxx\", resp.StatusCode/100)\n\t}\n\tgithubAPIRequestsTotal.WithLabelValues(path, method, statusClass).Inc()\n\tgithubAPIRequestDuration.WithLabelValues(path, method, statusClass).Observe(time.Since(start).Seconds())\n\n\tif err != nil {\n\t\tgithubAPIErrorsTotal.WithLabelValues(path, method, classifyGitHubAPIError(err)).Inc()\n\t\treturn resp, err\n\t}\n\n\tif resp != nil {\n\t\tcacheResult := \"miss\"\n\t\tif resp.Header.Get(httpcache.XFromCache) == \"1\" {\n\t\t\tcacheResult = \"hit\"\n\t\t}\n\t\tgithubAPICacheTotal.WithLabelValues(path, method, cacheResult).Inc()\n\t}\n\n\treturn resp, err\n}\n\nfunc classifyGitHubAPIError(err error) string {\n\tif errors.Is(err, context.Canceled) {\n\t\treturn \"canceled\"\n\t}\n\tif errors.Is(err, context.DeadlineExceeded) {\n\t\treturn \"deadline_exceeded\"\n\t}\n\tvar netErr net.Error\n\tif errors.As(err, &netErr) && netErr.Timeout() {\n\t\treturn \"timeout\"\n\t}\n\treturn \"transport\"\n}\n"
  },
  {
    "path": "pkg/gh/metrics_test.go",
    "content": "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/client_golang/prometheus/testutil\"\n)\n\ntype stubTransport struct {\n\tresp *http.Response\n\terr  error\n}\n\nfunc (s *stubTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tif s.resp != nil && s.resp.Request == nil {\n\t\ts.resp.Request = req\n\t}\n\treturn s.resp, s.err\n}\n\ntype timeoutErr struct{}\n\nfunc (timeoutErr) Error() string   { return \"timeout\" }\nfunc (timeoutErr) Timeout() bool   { return true }\nfunc (timeoutErr) Temporary() bool { return true }\n\nfunc TestInstrumentedTransportMetrics(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, \"https://api.github.com/repos/org/repo\", nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tpath := req.URL.Path\n\tmethod := req.Method\n\n\tresp := &http.Response{\n\t\tStatusCode: http.StatusOK,\n\t\tHeader:     make(http.Header),\n\t\tBody:       io.NopCloser(bytes.NewBufferString(\"ok\")),\n\t}\n\n\ttransport := newInstrumentedTransport(&stubTransport{resp: resp})\n\n\tbaseReq := testutil.ToFloat64(githubAPIRequestsTotal.WithLabelValues(path, method, \"2xx\"))\n\tbaseCache := testutil.ToFloat64(githubAPICacheTotal.WithLabelValues(path, method, \"miss\"))\n\tbaseInflight := testutil.ToFloat64(githubAPIInflight.WithLabelValues(path, method))\n\n\tif _, err := transport.RoundTrip(req); err != nil {\n\t\tt.Fatalf(\"RoundTrip error: %v\", err)\n\t}\n\n\tif got := testutil.ToFloat64(githubAPIRequestsTotal.WithLabelValues(path, method, \"2xx\")); got != baseReq+1 {\n\t\tt.Fatalf(\"requests_total mismatch: got=%v want=%v\", got, baseReq+1)\n\t}\n\tif got := testutil.ToFloat64(githubAPICacheTotal.WithLabelValues(path, method, \"miss\")); got != baseCache+1 {\n\t\tt.Fatalf(\"cache_total miss mismatch: got=%v want=%v\", got, baseCache+1)\n\t}\n\tif got := testutil.ToFloat64(githubAPIInflight.WithLabelValues(path, method)); got != baseInflight {\n\t\tt.Fatalf(\"inflight mismatch: got=%v want=%v\", got, baseInflight)\n\t}\n}\n\nfunc TestInstrumentedTransportCacheHit(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, \"https://api.github.com/repos/org/repo\", nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tpath := req.URL.Path\n\tmethod := req.Method\n\n\tresp := &http.Response{\n\t\tStatusCode: http.StatusOK,\n\t\tHeader:     make(http.Header),\n\t\tBody:       io.NopCloser(bytes.NewBufferString(\"cached\")),\n\t}\n\tresp.Header.Set(httpcache.XFromCache, \"1\")\n\n\ttransport := newInstrumentedTransport(&stubTransport{resp: resp})\n\n\tbaseCache := testutil.ToFloat64(githubAPICacheTotal.WithLabelValues(path, method, \"hit\"))\n\n\tif _, err := transport.RoundTrip(req); err != nil {\n\t\tt.Fatalf(\"RoundTrip error: %v\", err)\n\t}\n\n\tif got := testutil.ToFloat64(githubAPICacheTotal.WithLabelValues(path, method, \"hit\")); got != baseCache+1 {\n\t\tt.Fatalf(\"cache_total hit mismatch: got=%v want=%v\", got, baseCache+1)\n\t}\n}\n\nfunc TestInstrumentedTransportErrorMetrics(t *testing.T) {\n\treq, err := http.NewRequest(http.MethodGet, \"https://api.github.com/repos/org/repo\", nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\tpath := req.URL.Path\n\tmethod := req.Method\n\n\ttransport := newInstrumentedTransport(&stubTransport{err: timeoutErr{}})\n\n\tbaseReq := testutil.ToFloat64(githubAPIRequestsTotal.WithLabelValues(path, method, \"error\"))\n\tbaseErr := testutil.ToFloat64(githubAPIErrorsTotal.WithLabelValues(path, method, \"timeout\"))\n\n\tif _, err := transport.RoundTrip(req); err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\n\tif got := testutil.ToFloat64(githubAPIRequestsTotal.WithLabelValues(path, method, \"error\")); got != baseReq+1 {\n\t\tt.Fatalf(\"requests_total error mismatch: got=%v want=%v\", got, baseReq+1)\n\t}\n\tif got := testutil.ToFloat64(githubAPIErrorsTotal.WithLabelValues(path, method, \"timeout\")); got != baseErr+1 {\n\t\tt.Fatalf(\"errors_total mismatch: got=%v want=%v\", got, baseErr+1)\n\t}\n}\n"
  },
  {
    "path": "pkg/gh/ratelimit.go",
    "content": "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, rateLimit github.Rate) {\n\tif rateLimit.Reset.IsZero() {\n\t\t// Not configure rate limit, don't need to store (e.g. GHES)\n\t\treturn\n\t}\n\n\trateLimitLimit.Store(scope, rateLimit.Limit)\n\trateLimitRemain.Store(scope, rateLimit.Remaining)\n}\n\nfunc getRateLimitKey(org, repo string) string {\n\tif repo == \"\" {\n\t\treturn org\n\t}\n\treturn fmt.Sprintf(\"%s/%s\", org, repo)\n}\n\n// GetRateLimitRemain get a list of rate limit remaining\n// key: scope, value: remain\nfunc GetRateLimitRemain() map[string]int {\n\tm := map[string]int{}\n\tmu := sync.Mutex{}\n\n\trateLimitRemain.Range(func(key, value interface{}) bool {\n\t\tk, ok := key.(string)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tv, ok := value.(int)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tmu.Lock()\n\t\tm[k] = v\n\t\tmu.Unlock()\n\t\treturn true\n\t})\n\n\treturn m\n}\n\n// GetRateLimitLimit get a list of rate limit\n// key: scope, value: remain\nfunc GetRateLimitLimit() map[string]int {\n\tm := map[string]int{}\n\tmu := sync.Mutex{}\n\n\trateLimitLimit.Range(func(key, value interface{}) bool {\n\t\tk, ok := key.(string)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tv, ok := value.(int)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tmu.Lock()\n\t\tm[k] = v\n\t\tmu.Unlock()\n\t\treturn true\n\t})\n\n\treturn m\n}\n"
  },
  {
    "path": "pkg/gh/runner.go",
    "content": "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/whywaita/myshoes/pkg/logger\"\n)\n\n// ExistGitHubRunner check exist registered of GitHub runner\nfunc ExistGitHubRunner(ctx context.Context, client *github.Client, owner, repo, runnerName string) (*github.Runner, error) {\n\trunners, err := ListRunners(ctx, client, owner, repo)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get list of runners: %w\", err)\n\t}\n\n\treturn ExistGitHubRunnerWithRunner(runners, runnerName)\n}\n\n// ExistGitHubRunnerWithRunner check exist registered of GitHub runner from a list of runner\nfunc ExistGitHubRunnerWithRunner(runners []*github.Runner, runnerName string) (*github.Runner, error) {\n\tfor _, r := range runners {\n\t\tif strings.EqualFold(r.GetName(), runnerName) {\n\t\t\treturn r, nil\n\t\t}\n\t}\n\n\treturn nil, ErrNotFound\n}\n\n// ListRunners get runners that registered repository or org\nfunc ListRunners(ctx context.Context, client *github.Client, owner, repo string) ([]*github.Runner, error) {\n\tif cachedRs, found := responseCache.Get(getCacheKey(owner, repo)); found {\n\t\treturn cachedRs.([]*github.Runner), nil\n\t}\n\n\tvar opts = &github.ListRunnersOptions{\n\t\tListOptions: github.ListOptions{\n\t\t\tPage:    0,\n\t\t\tPerPage: 100,\n\t\t},\n\t}\n\n\tvar rs []*github.Runner\n\tfor {\n\t\tlogger.Logf(true, \"get runners from GitHub, page: %d, now all runners: %d\", opts.Page, len(rs))\n\t\trunners, resp, err := listRunners(ctx, client, owner, repo, opts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list runners: %w\", err)\n\t\t}\n\t\tstoreRateLimit(getRateLimitKey(owner, repo), resp.Rate)\n\n\t\trs = append(rs, runners.Runners...)\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\topts.ListOptions.Page = resp.NextPage\n\t}\n\n\tresponseCache.Set(getCacheKey(owner, repo), rs, 1*time.Second)\n\tlogger.Logf(true, \"found %d runners in GitHub\", len(rs))\n\n\treturn rs, nil\n}\n\nfunc getCacheKey(owner, repo string) string {\n\treturn fmt.Sprintf(\"owner-%s-repo-%s\", owner, repo)\n}\n\nfunc listRunners(ctx context.Context, client *github.Client, owner, repo string, opts *github.ListRunnersOptions) (*github.Runners, *github.Response, error) {\n\tif repo == \"\" {\n\t\trunners, resp, err := client.Actions.ListOrganizationRunners(ctx, owner, opts)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to list organization runners: %w\", err)\n\t\t}\n\t\treturn runners, resp, nil\n\t}\n\n\trunners, resp, err := client.Actions.ListRunners(ctx, owner, repo, opts)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to list repository runners: %w\", err)\n\t}\n\treturn runners, resp, nil\n}\n\n// GetLatestRunnerVersion get a latest version of actions/runner\nfunc GetLatestRunnerVersion(ctx context.Context, scope string) (string, error) {\n\tclientApps, err := NewClientGitHubApps()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create a client from Apps: %+v\", err)\n\t}\n\tinstallationID, err := IsInstalledGitHubApp(ctx, scope)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get installlation id: %w\", err)\n\t}\n\ttoken, _, err := GenerateGitHubAppsToken(ctx, clientApps, installationID, scope)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get registration token: %w\", err)\n\t}\n\tclient, err := NewClient(token)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create GitHub client: %w\", err)\n\t}\n\n\tswitch DetectScope(scope) {\n\tcase Repository:\n\t\towner, repo := DivideScope(scope)\n\t\tapplications, resp, err := client.Actions.ListRunnerApplicationDownloads(ctx, owner, repo)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get latest runner version: %w\", err)\n\t\t}\n\t\tstoreRateLimit(getRateLimitKey(owner, repo), resp.Rate)\n\t\treturn getRunnerVersion(applications)\n\tcase Organization:\n\t\tapplications, resp, err := client.Actions.ListOrganizationRunnerApplicationDownloads(ctx, scope)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get latest runner version: %w\", err)\n\t\t}\n\t\tstoreRateLimit(getRateLimitKey(scope, \"\"), resp.Rate)\n\t\treturn getRunnerVersion(applications)\n\t}\n\treturn \"\", fmt.Errorf(\"invalid scope: %s\", scope)\n}\n\nfunc getRunnerVersion(applications []*github.RunnerApplicationDownload) (string, error) {\n\t// filename\": \"actions-runner-linux-x64-2.164.0.tar.gz\"\n\tfor _, app := range applications {\n\t\tif *app.OS == \"linux\" && *app.Architecture == \"x64\" {\n\t\t\tv := strings.ReplaceAll(*app.Filename, \"actions-runner-linux-x64-\", \"\")\n\t\t\tv = strings.ReplaceAll(v, \".tar.gz\", \"\")\n\t\t\treturn fmt.Sprintf(\"v%s\", v), nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"not found runner version\")\n}\n\n// ConcatLabels concat labels from check event JSON\nfunc ConcatLabels(checkEventJSON string) (string, error) {\n\trunsOnLabels, err := ExtractRunsOnLabels([]byte(checkEventJSON))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to extract runs-on labels: %w\", err)\n\t}\n\n\trunsOnConcat := \"none\"\n\tif len(runsOnLabels) != 0 {\n\t\trunsOnConcat = strings.Join(runsOnLabels, \",\") // e.g. \"self-hosted,linux\"\n\t}\n\treturn runsOnConcat, nil\n}\n"
  },
  {
    "path": "pkg/gh/scope.go",
    "content": "package gh\n\nimport \"strings\"\n\n// Scope is scope for auto-scaling target\ntype Scope int\n\n// Scope values\nconst (\n\tUnknown Scope = iota\n\tRepository\n\tOrganization\n)\n\n// String is fmt.Stringer interface\nfunc (s Scope) String() string {\n\tswitch s {\n\tcase Repository:\n\t\treturn \"repos\"\n\tcase Organization:\n\t\treturn \"orgs\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// DetectScope detect a scope (repo or org)\nfunc DetectScope(scope string) Scope {\n\tsep := strings.Split(scope, \"/\")\n\tswitch len(sep) {\n\tcase 1:\n\t\treturn Organization\n\tcase 2:\n\t\treturn Repository\n\tdefault:\n\t\treturn Unknown\n\t}\n}\n\n// DivideScope divide scope to owner and repo\nfunc DivideScope(scope string) (string, string) {\n\tvar owner, repo string\n\n\tswitch DetectScope(scope) {\n\tcase Organization:\n\t\towner = scope\n\t\trepo = \"\"\n\tcase Repository:\n\t\ts := strings.Split(scope, \"/\")\n\t\towner = s[0]\n\t\trepo = s[1]\n\t}\n\n\treturn owner, repo\n}\n"
  },
  {
    "path": "pkg/gh/token_registration.go",
    "content": "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 = cache.New(1*time.Hour, 1*time.Hour)\n)\n\n// GetRunnerRegistrationToken get token for register runner\n// clientInstallation needs to response of `NewClientInstallation()`\nfunc GetRunnerRegistrationToken(ctx context.Context, installationID int64, scope string) (string, error) {\n\tcachedToken := getRunnerRegisterTokenFromCache(installationID, scope)\n\tif cachedToken != \"\" {\n\t\treturn cachedToken, nil\n\t}\n\n\trrToken, expiresAt, err := generateRunnerRegisterToken(ctx, installationID, scope)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate runner register token: %w\", err)\n\t}\n\tsetRunnerRegisterTokenCache(installationID, scope, rrToken, *expiresAt)\n\treturn rrToken, nil\n}\n\n// generateRunnerRegistrationToken generate token for register runner\n// clientInstallation needs to response of `NewClientInstallation()`\nfunc generateRunnerRegisterToken(ctx context.Context, installationID int64, scope string) (string, *time.Time, error) {\n\tclientInstallation, err := NewClientInstallation(installationID)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"failed to create a client installation: %w\", err)\n\t}\n\n\tswitch DetectScope(scope) {\n\tcase Organization:\n\t\ttoken, _, err := clientInstallation.Actions.CreateOrganizationRegistrationToken(ctx, scope)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to generate registration token for organization (scope: %s): %w\", scope, err)\n\t\t}\n\t\treturn *token.Token, &token.ExpiresAt.Time, nil\n\tcase Repository:\n\t\towner, repo := DivideScope(scope)\n\t\ttoken, _, err := clientInstallation.Actions.CreateRegistrationToken(ctx, owner, repo)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to generate registration token for repository (scope: %s): %w\", scope, err)\n\t\t}\n\t\treturn *token.Token, &token.ExpiresAt.Time, nil\n\tdefault:\n\t\treturn \"\", nil, fmt.Errorf(\"failed to detect scope (scope: %s)\", scope)\n\t}\n}\n\nfunc setRunnerRegisterTokenCache(installationID int64, scope, token string, expiresAt time.Time) {\n\texpiresDuration := time.Until(expiresAt.Add(-6 * time.Minute))\n\n\tcacheRegistrationToken.Set(getCacheKeyRegistrationToken(installationID, scope), token, expiresDuration)\n}\n\nfunc getRunnerRegisterTokenFromCache(installationID int64, scope string) string {\n\tgot, found := cacheRegistrationToken.Get(getCacheKeyRegistrationToken(installationID, scope))\n\tif !found {\n\t\treturn \"\"\n\t}\n\ttoken, ok := got.(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\treturn token\n}\n\nfunc getCacheKeyRegistrationToken(installationID int64, scope string) string {\n\treturn fmt.Sprintf(\"%s-%d\", scope, installationID)\n}\n"
  },
  {
    "path": "pkg/gh/webhook.go",
    "content": "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 json of webhook from GitHub.\n// github.ParseWebHook need *http.Request because it checks headers in request.\nfunc parseEventJSON(in []byte) (interface{}, error) {\n\tvar checkRun *github.CheckRunEvent\n\terr := json.Unmarshal(in, &checkRun)\n\tif err == nil && checkRun.GetCheckRun() != nil {\n\t\treturn checkRun, nil\n\t}\n\n\tvar workflowJobEvent *github.WorkflowJobEvent\n\terr = json.Unmarshal(in, &workflowJobEvent)\n\tif err == nil && workflowJobEvent.GetWorkflowJob() != nil {\n\t\treturn workflowJobEvent, nil\n\t}\n\n\tvar workflowJob *github.WorkflowJob\n\terr = json.Unmarshal(in, &workflowJob)\n\tif err == nil && workflowJob != nil {\n\t\treturn workflowJob, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"input json is unsupported type\")\n}\n\n// ExtractRunsOnLabels extract labels from github.WorkflowJobEvent\nfunc ExtractRunsOnLabels(in []byte) ([]string, error) {\n\tevent, err := parseEventJSON(in)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse event json: %w\", err)\n\t}\n\n\tswitch t := event.(type) {\n\tcase *github.WorkflowJobEvent:\n\t\t// workflow_job has labels, can extract labels\n\t\treturn t.GetWorkflowJob().Labels, nil\n\tcase *github.WorkflowJob:\n\t\treturn t.Labels, nil\n\t}\n\n\treturn []string{}, nil\n}\n"
  },
  {
    "path": "pkg/gh/workflow_job.go",
    "content": "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 context.Context, client *github.Client, owner, repo string, runID int64, opts *github.ListWorkflowJobsOptions) ([]*github.WorkflowJob, *github.Response, error) {\n\tjobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, opts)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to list workflow runs: %w\", err)\n\t}\n\treturn jobs.Jobs, resp, nil\n}\n\n// ListWorkflowJobByRunID get workflow job by run ID\nfunc ListWorkflowJobByRunID(ctx context.Context, client *github.Client, owner, repo string, runID int64) ([]*github.WorkflowJob, error) {\n\tif cachedWorkflowJobs, found := responseCache.Get(getWorkflowJobCacheKey(owner, repo, runID)); found {\n\t\treturn cachedWorkflowJobs.([]*github.WorkflowJob), nil\n\t}\n\topts := &github.ListWorkflowJobsOptions{\n\t\tFilter: \"latest\",\n\t}\n\tjobs, _, err := listWorkflowJob(ctx, client, owner, repo, runID, opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list workflow jobs: %w\", err)\n\t}\n\n\tresponseCache.Set(getWorkflowJobCacheKey(owner, repo, runID), jobs, 1*time.Minute)\n\treturn jobs, nil\n}\n\nfunc getWorkflowJobCacheKey(owner, repo string, runID int64) string {\n\treturn fmt.Sprintf(\"runs-owner-%s-repo-%s-runid-%d\", owner, repo, runID)\n}\n"
  },
  {
    "path": "pkg/gh/workflow_run.go",
    "content": "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/pkg/logger\"\n)\n\nfunc listWorkflowRuns(ctx context.Context, client *github.Client, owner, repo string, opts *github.ListWorkflowRunsOptions) (*github.WorkflowRuns, *github.Response, error) {\n\truns, resp, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to list workflow runs: %w\", err)\n\t}\n\treturn runs, resp, nil\n}\n\n// ListWorkflowRunsNewest get workflow runs that registered in the last (%d: limit) runs\nfunc ListWorkflowRunsNewest(ctx context.Context, client *github.Client, owner, repo string, limit int) ([]*github.WorkflowRun, error) {\n\tif cachedWorkflowRuns, found := responseCache.Get(getRunsCacheKey(owner, repo)); found {\n\t\treturn cachedWorkflowRuns.([]*github.WorkflowRun), nil\n\t}\n\n\tvar opts = &github.ListWorkflowRunsOptions{\n\t\tListOptions: github.ListOptions{\n\t\t\tPage:    0,\n\t\t\tPerPage: 100,\n\t\t},\n\t}\n\n\tvar workflowRuns []*github.WorkflowRun\n\tfor {\n\t\tlogger.Logf(true, \"get workflow runs from GitHub, page: %d, now all runners: %d (repo: %s/%s)\", opts.Page, len(workflowRuns), owner, repo)\n\t\truns, resp, err := listWorkflowRuns(ctx, client, owner, repo, opts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to list workflow runs: %w\", err)\n\t\t}\n\t\tstoreRateLimit(getRateLimitKey(owner, repo), resp.Rate)\n\n\t\tworkflowRuns = append(workflowRuns, runs.WorkflowRuns...)\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\tif len(workflowRuns) >= limit {\n\t\t\tbreak\n\t\t}\n\n\t\topts.Page = resp.NextPage\n\t}\n\n\tresponseCache.Set(getRunsCacheKey(owner, repo), workflowRuns, 3*time.Minute)\n\treturn workflowRuns, nil\n}\n\nfunc getRunsCacheKey(owner, repo string) string {\n\treturn fmt.Sprintf(\"runs-owner-%s-repo-%s\", owner, repo)\n}\n"
  },
  {
    "path": "pkg/logger/logger.go",
    "content": "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.Stderr, \"\", log.LstdFlags)\n\tlogMu  sync.Mutex\n)\n\n// SetLogger set logger in outside of library\nfunc SetLogger(l *log.Logger) {\n\tif l == nil {\n\t\tl = log.New(os.Stderr, \"\", log.LstdFlags)\n\t}\n\tlogMu.Lock()\n\tlogger = l\n\tlogMu.Unlock()\n}\n\n// Logf is interface for logger\nfunc Logf(isDebug bool, format string, v ...interface{}) {\n\tlogMu.Lock()\n\tdefer logMu.Unlock()\n\n\tswitch {\n\tcase !isDebug:\n\t\t// normal logging\n\t\tlogger.Printf(format, v...)\n\tcase isDebug && config.Config.Debug:\n\t\t// debug logging\n\t\tformat = \"[DEBUG] \" + format\n\t\tlogger.Printf(format, v...)\n\t}\n}\n"
  },
  {
    "path": "pkg/metric/collector.go",
    "content": "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.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\nconst (\n\tnamespace = \"myshoes\"\n)\n\nvar (\n\tscrapeDurationDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, \"\", \"collector_duration_seconds\"),\n\t\t\"Collector time duration.\",\n\t\t[]string{\"collector\"}, nil,\n\t)\n)\n\n// Collector is a collector for prometheus\ntype Collector struct {\n\tctx      context.Context\n\tmetrics  Metrics\n\tds       datastore.Datastore\n\tscrapers []Scraper\n}\n\n// NewCollector create a collector\nfunc NewCollector(ctx context.Context, ds datastore.Datastore) *Collector {\n\treturn &Collector{\n\t\tctx:      ctx,\n\t\tmetrics:  NewMetrics(),\n\t\tds:       ds,\n\t\tscrapers: NewScrapers(),\n\t}\n}\n\n// Describe describe metrics\nfunc (c *Collector) Describe(ch chan<- *prometheus.Desc) {\n\tch <- c.metrics.TotalScrapes.Desc()\n\tch <- c.metrics.Error.Desc()\n\tc.metrics.ScrapeErrors.Describe(ch)\n}\n\n// Collect collect metrics\nfunc (c *Collector) Collect(ch chan<- prometheus.Metric) {\n\tc.scrape(c.ctx, ch)\n\n\tch <- c.metrics.TotalScrapes\n\tch <- c.metrics.Error\n\tc.metrics.ScrapeErrors.Collect(ch)\n}\n\nfunc (c *Collector) scrape(ctx context.Context, ch chan<- prometheus.Metric) {\n\tc.metrics.TotalScrapes.Inc()\n\tc.metrics.Error.Set(0)\n\n\tvar wg sync.WaitGroup\n\tfor _, scraper := range c.scrapers {\n\t\twg.Add(1)\n\t\tgo func(scraper Scraper) {\n\t\t\tdefer wg.Done()\n\t\t\tlabel := fmt.Sprintf(\"collect.%s\", scraper.Name())\n\t\t\tscrapeStartTime := time.Now()\n\t\t\tif err := scraper.Scrape(ctx, c.ds, ch); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to scrape metrics (name: %s): %+v\", scraper.Name(), err)\n\t\t\t\tc.metrics.ScrapeErrors.WithLabelValues(label).Inc()\n\t\t\t\tc.metrics.Error.Set(1)\n\t\t\t}\n\t\t\tch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, time.Since(scrapeStartTime).Seconds(), label)\n\t\t}(scraper)\n\t}\n\twg.Wait()\n}\n\n// Scraper is interface for scraping\ntype Scraper interface {\n\tName() string\n\tHelp() string\n\tScrape(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error\n}\n\n// NewScrapers return list of scraper\nfunc NewScrapers() []Scraper {\n\treturn []Scraper{\n\t\tScraperDatastore{},\n\t\tScraperMemory{},\n\t\tScraperGitHub{},\n\t}\n}\n\n// Metrics is data in scraper\ntype Metrics struct {\n\tTotalScrapes prometheus.Counter\n\tScrapeErrors *prometheus.CounterVec\n\tError        prometheus.Gauge\n}\n\n// NewMetrics create a metrics\nfunc NewMetrics() Metrics {\n\treturn Metrics{\n\t\tTotalScrapes: prometheus.NewCounter(prometheus.CounterOpts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: \"\",\n\t\t\tName:      \"scrapes_total\",\n\t\t\tHelp:      \"Total number of times myshoes was scraped for metrics.\",\n\t\t}),\n\t\tScrapeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: \"\",\n\t\t\tName:      \"scrape_errors_total\",\n\t\t\tHelp:      \"Total number of times an error occurred scraping a myshoes.\",\n\t\t}, []string{\"collector\"}),\n\t\tError: prometheus.NewGauge(prometheus.GaugeOpts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: \"\",\n\t\t\tName:      \"last_scrape_error\",\n\t\t\tHelp:      \"Whether the last scrape of metrics from myshoes resulted in an error (1 for error, 0 for success).\",\n\t\t}),\n\t}\n}\n"
  },
  {
    "path": "pkg/metric/scrape_datastore.go",
    "content": "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\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\t\"github.com/whywaita/myshoes/pkg/starter\"\n)\n\nconst datastoreName = \"datastore\"\n\nvar (\n\tdatastoreJobsDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"jobs\"),\n\t\t\"Number of jobs\",\n\t\t[]string{\"target_id\", \"runs_on\"}, nil,\n\t)\n\tdatastoreTargetsDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"targets\"),\n\t\t\"Number of targets\",\n\t\t[]string{\"resource_type\"}, nil,\n\t)\n\tdatastoreTargetDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"target_describe\"),\n\t\t\"Target\",\n\t\t[]string{\n\t\t\t\"target_id\",\n\t\t\t\"scope\",\n\t\t\t\"resource_type\",\n\t\t}, nil,\n\t)\n\tdatastoreTargetTokenExpiresDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"target_token_expires_seconds\"),\n\t\t\"Token expires time\",\n\t\t[]string{\"target_id\"}, nil,\n\t)\n\tdatastoreJobDurationOldest = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"job_duration_oldest_seconds\"),\n\t\t\"Duration time of oldest job\",\n\t\t[]string{\"job_id\", \"runs_on\"}, nil,\n\t)\n\tdatastoreDeletedJobsDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"deleted_jobs\"),\n\t\t\"Number of deleted jobs\",\n\t\t[]string{\"runs_on\"}, nil,\n\t)\n\tdatastoreRunnersRunningDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, datastoreName, \"runners_running\"),\n\t\t\"Number of runners running\",\n\t\t[]string{\"target_id\"}, nil,\n\t)\n)\n\n// ScraperDatastore is scraper implement for datastore.Datastore\ntype ScraperDatastore struct{}\n\n// Name return name\nfunc (ScraperDatastore) Name() string {\n\treturn datastoreName\n}\n\n// Help return help\nfunc (ScraperDatastore) Help() string {\n\treturn \"Collect from datastore\"\n}\n\n// Scrape scrape metrics\nfunc (ScraperDatastore) Scrape(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\tif err := scrapeJobs(ctx, ds, ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape jobs: %w\", err)\n\t}\n\tif err := scrapeTargets(ctx, ds, ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape targets: %w\", err)\n\t}\n\tif err := scrapeRunners(ctx, ds, ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape runners: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc scrapeJobs(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\tif err := scrapeJobCounter(ctx, ds, ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape job counter: %w\", err)\n\t}\n\n\tjobs, err := ds.ListJobs(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list jobs: %w\", err)\n\t}\n\n\tif len(jobs) == 0 {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreJobsDesc, prometheus.GaugeValue, 0, \"none\", \"none\",\n\t\t)\n\t\treturn nil\n\t}\n\n\tsort.SliceStable(jobs, func(i, j int) bool {\n\t\t// oldest job is first\n\t\treturn jobs[i].CreatedAt.Before(jobs[j].CreatedAt)\n\t})\n\ttype storedValue struct {\n\t\tOldestJob datastore.Job\n\t\tCount     float64\n\t}\n\n\tstored := map[string]storedValue{}\n\t// job separate target_id and runs-on labels\n\tfor _, j := range jobs {\n\t\trunsOnConcat, err := gh.ConcatLabels(j.CheckEventJSON)\n\t\tif err != nil {\n\t\t\tlogger.Logf(false, \"failed to concat labels: %+v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tkey := fmt.Sprintf(\"%s-_-%s\", j.TargetID.String(), runsOnConcat)\n\t\tv, ok := stored[key]\n\t\tif !ok {\n\t\t\tstored[key] = storedValue{\n\t\t\t\tOldestJob: j,\n\t\t\t\tCount:     1,\n\t\t\t}\n\t\t} else {\n\t\t\tif j.CreatedAt.Before(v.OldestJob.CreatedAt) {\n\t\t\t\tstored[key] = storedValue{\n\t\t\t\t\tOldestJob: j,\n\t\t\t\t\tCount:     v.Count + 1,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tstored[key] = storedValue{\n\t\t\t\t\tOldestJob: v.OldestJob,\n\t\t\t\t\tCount:     v.Count + 1,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor key, value := range stored {\n\t\t// key: target_id-_-runs-on\n\t\t// value: storedValue\n\n\t\tsplit := strings.Split(key, \"-_-\")\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreJobsDesc, prometheus.GaugeValue,\n\t\t\tvalue.Count,\n\t\t\tsplit[0], // target_id\n\t\t\tsplit[1], // runs-on\n\t\t)\n\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreJobDurationOldest,\n\t\t\tprometheus.GaugeValue,\n\t\t\ttime.Since(value.OldestJob.CreatedAt).Seconds(),\n\t\t\tvalue.OldestJob.UUID.String(),\n\t\t\tsplit[1],\n\t\t)\n\t}\n\n\treturn nil\n}\n\nfunc scrapeJobCounter(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\tstarter.DeletedJobMap.Range(func(key, value interface{}) bool {\n\t\trunsOn := key.(string)\n\t\tnumber := value.(int)\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreDeletedJobsDesc, prometheus.CounterValue, float64(number), runsOn,\n\t\t)\n\t\treturn true\n\t})\n\treturn nil\n}\n\nfunc scrapeTargets(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\ttargets, err := datastore.ListTargets(ctx, ds)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list targets: %w\", err)\n\t}\n\n\tnow := time.Now()\n\tresult := map[string]float64{} // key: resource_type, value: number\n\tfor _, t := range targets {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreTargetDesc, prometheus.GaugeValue, 1,\n\t\t\tt.UUID.String(), t.Scope, t.ResourceType.String(),\n\t\t)\n\n\t\tresult[t.ResourceType.String()]++\n\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreTargetTokenExpiresDesc, prometheus.GaugeValue,\n\t\t\tt.TokenExpiredAt.Sub(now).Seconds(),\n\t\t\tt.UUID.String(),\n\t\t)\n\t}\n\tfor rt, number := range result {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreTargetsDesc, prometheus.GaugeValue, number, rt,\n\t\t)\n\t}\n\n\treturn nil\n}\n\nfunc scrapeRunners(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\trunners, err := ds.ListRunners(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list runners: %w\", err)\n\t}\n\n\tresult := map[string]float64{} // key: target_id, value: number\n\tfor _, r := range runners {\n\t\tresult[r.TargetID.String()]++\n\t}\n\tfor targetID, number := range result {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tdatastoreRunnersRunningDesc, prometheus.GaugeValue, number, targetID,\n\t\t)\n\t}\n\n\treturn nil\n}\n\nvar _ Scraper = ScraperDatastore{}\n"
  },
  {
    "path": "pkg/metric/scrape_github.go",
    "content": "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\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\nconst githubName = \"github\"\n\nvar (\n\tgithubPendingRunsDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, githubName, \"pending_runs\"),\n\t\t\"Number of pending runs\",\n\t\t[]string{\"target_id\", \"scope\"}, nil,\n\t)\n\tgithubPendingWorkflowRunSecondsDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, githubName, \"pending_workflow_run_seconds\"),\n\t\t\"Second of Pending time in workflow run\",\n\t\t[]string{\"target_id\", \"workflow_id\", \"workflow_run_id\", \"html_url\"}, nil,\n\t)\n\tgithubInstallationDesc = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, githubName, \"installation\"),\n\t\t\"installations\",\n\t\t[]string{\n\t\t\t\"installation_id\",\n\t\t\t\"account_login\",\n\t\t\t\"account_type\",\n\t\t\t\"target_type\",\n\t\t\t\"repository_selection\",\n\t\t\t\"html_url\",\n\t\t},\n\t\tnil,\n\t)\n)\n\n// ScraperGitHub is scraper implement for GitHub\ntype ScraperGitHub struct{}\n\n// Name return name\nfunc (ScraperGitHub) Name() string {\n\treturn githubName\n}\n\n// Help return help\nfunc (ScraperGitHub) Help() string {\n\treturn \"Collect from GitHub\"\n}\n\n// Scrape scrape metrics\nfunc (s ScraperGitHub) Scrape(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\tif err := scrapePendingRuns(ctx, ds, ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape pending runs: %w\", err)\n\t}\n\tif err := scrapeInstallation(ctx, ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape installations: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc scrapePendingRuns(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\tpendingRuns, err := datastore.GetPendingWorkflowRunByRecentRepositories(ctx, ds)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get pending workflow runs: %w\", err)\n\t}\n\n\tfor _, pendingRun := range pendingRuns {\n\t\tsinceSeconds := time.Since(pendingRun.WorkflowRun.CreatedAt.Time).Seconds()\n\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tgithubPendingWorkflowRunSecondsDesc,\n\t\t\tprometheus.GaugeValue,\n\t\t\tsinceSeconds,\n\t\t\tpendingRun.Target.UUID.String(),\n\t\t\tstrconv.FormatInt(pendingRun.WorkflowRun.GetWorkflowID(), 10),\n\t\t\tstrconv.FormatInt(pendingRun.WorkflowRun.GetID(), 10),\n\t\t\tpendingRun.WorkflowRun.GetHTMLURL(),\n\t\t)\n\t}\n\n\t// count pending runs by target\n\tcountPendingMap := make(map[string]int)\n\ttargetCache := make(map[string]*datastore.Target)\n\tfor _, pendingRun := range pendingRuns {\n\t\tcountPendingMap[pendingRun.Target.UUID.String()]++\n\t\ttargetCache[pendingRun.Target.UUID.String()] = pendingRun.Target\n\t}\n\n\tfor targetID, countPending := range countPendingMap {\n\t\ttarget, ok := targetCache[targetID]\n\t\tif !ok {\n\t\t\tlogger.Logf(false, \"failed to get target by targetID from targetCache: %s\", targetID)\n\t\t\tcontinue\n\t\t}\n\t\tch <- prometheus.MustNewConstMetric(githubPendingRunsDesc, prometheus.GaugeValue, float64(countPending), target.UUID.String(), target.Scope)\n\t}\n\treturn nil\n}\n\nfunc scrapeInstallation(ctx context.Context, ch chan<- prometheus.Metric) error {\n\tinstallations, err := gh.GHlistInstallations(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list installations: %w\", err)\n\t}\n\n\tfor _, installation := range installations {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tgithubInstallationDesc,\n\t\t\tprometheus.GaugeValue,\n\t\t\t1,\n\t\t\tfmt.Sprint(installation.GetID()),\n\t\t\tinstallation.GetAccount().GetLogin(),\n\t\t\tinstallation.GetAccount().GetType(),\n\t\t\tinstallation.GetTargetType(),\n\t\t\tinstallation.GetRepositorySelection(),\n\t\t\tinstallation.GetAccount().GetHTMLURL(),\n\t\t)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/metric/scrape_memory.go",
    "content": "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/client_golang/prometheus\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/docker\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/runner\"\n\t\"github.com/whywaita/myshoes/pkg/starter\"\n)\n\nconst memoryName = \"memory\"\n\nvar (\n\tmemoryStarterMaxRunning = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"starter_max_running\"),\n\t\t\"The number of max running in starter (Config)\",\n\t\t[]string{\"starter\"}, nil,\n\t)\n\tmemoryStarterQueueRunning = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"starter_queue_running\"),\n\t\t\"running queue in starter\",\n\t\t[]string{\"starter\"}, nil,\n\t)\n\tmemoryStarterQueueWaiting = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"starter_queue_waiting\"),\n\t\t\"waiting queue in starter\",\n\t\t[]string{\"starter\"}, nil,\n\t)\n\tmemoryStarterRescuedRuns = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"starter_rescued_runs\"),\n\t\t\"rescued runs in starter\",\n\t\t[]string{\"starter\", \"target\"}, nil,\n\t)\n\tmemoryGitHubRateLimitRemaining = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"github_rate_limit_remaining\"),\n\t\t\"The number of rate limit remaining in GitHub\",\n\t\t[]string{\"scope\"}, nil,\n\t)\n\tmemoryGitHubRateLimitLimiting = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"github_rate_limit_limiting\"),\n\t\t\"The number of rate limit max in GitHub\",\n\t\t[]string{\"scope\"}, nil,\n\t)\n\tmemoryDockerHubRateLimitRemaining = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"dockerhub_rate_limit_remaining\"),\n\t\t\"The number of rate limit remaining in DockerHub\",\n\t\t[]string{}, nil,\n\t)\n\tmemoryDockerHubRateLimitLimiting = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"dockerhub_rate_limit_limiting\"),\n\t\t\"The number of rate limit max in DockerHub\",\n\t\t[]string{}, nil,\n\t)\n\tmemoryRunnerMaxConcurrencyDeleting = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"runner_max_concurrency_deleting\"),\n\t\t\"The number of max concurrency deleting in runner (Config)\",\n\t\t[]string{\"runner\"}, nil,\n\t)\n\tmemoryRunnerQueueConcurrencyDeleting = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"runner_queue_concurrency_deleting\"),\n\t\t\"deleting concurrency in runner\",\n\t\t[]string{\"runner\"}, nil,\n\t)\n\tmemoryRunnerDeleteRetryCount = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"runner_delete_retry_count\"),\n\t\t\"retry count of deleting in runner\",\n\t\t[]string{\"runner\"}, nil,\n\t)\n\tmemoryRunnerCreateRetryCount = prometheus.NewDesc(\n\t\tprometheus.BuildFQName(namespace, memoryName, \"runner_create_retry_count\"),\n\t\t\"retry count of creating in runner\",\n\t\t[]string{\"runner\"}, nil,\n\t)\n)\n\n// ScraperMemory is scraper implement for memory\ntype ScraperMemory struct{}\n\n// Name return name\nfunc (ScraperMemory) Name() string {\n\treturn memoryName\n}\n\n// Help return help\nfunc (ScraperMemory) Help() string {\n\treturn \"Collect from memory\"\n}\n\n// Scrape scrape metrics\nfunc (ScraperMemory) Scrape(ctx context.Context, ds datastore.Datastore, ch chan<- prometheus.Metric) error {\n\tif err := scrapeStarterValues(ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape starter values: %w\", err)\n\t}\n\tif err := scrapeGitHubValues(ch); err != nil {\n\t\treturn fmt.Errorf(\"failed to scrape GitHub values: %w\", err)\n\t}\n\tif config.Config.ProvideDockerHubMetrics {\n\t\tif err := scrapeDockerValues(ch); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to scrape Docker values: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc scrapeStarterValues(ch chan<- prometheus.Metric) error {\n\tconfigMax := config.Config.MaxConnectionsToBackend\n\n\tconst labelStarter = \"starter\"\n\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryStarterMaxRunning, prometheus.GaugeValue, float64(configMax), labelStarter)\n\n\tcountRunning := starter.CountRunning.Load()\n\tcountWaiting := starter.CountWaiting.Load()\n\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryStarterQueueRunning, prometheus.GaugeValue, float64(countRunning), labelStarter)\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryStarterQueueWaiting, prometheus.GaugeValue, float64(countWaiting), labelStarter)\n\n\tstarter.CountRescued.Range(func(key, value interface{}) bool {\n\t\tcounter := value.(*atomic.Int64)\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tmemoryStarterRescuedRuns, prometheus.GaugeValue, float64(counter.Load()), labelStarter, key.(string),\n\t\t)\n\t\treturn true\n\t})\n\n\tconst labelRunner = \"runner\"\n\n\tconfigRunnerDeletingMax := config.Config.MaxConcurrencyDeleting\n\tcountRunnerDeletingNow := runner.ConcurrencyDeleting.Load()\n\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryRunnerMaxConcurrencyDeleting, prometheus.GaugeValue, float64(configRunnerDeletingMax), labelRunner)\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryRunnerQueueConcurrencyDeleting, prometheus.GaugeValue, float64(countRunnerDeletingNow), labelRunner)\n\n\trunner.DeleteRetryCount.Range(func(key, value any) bool {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tmemoryRunnerDeleteRetryCount, prometheus.GaugeValue, float64(value.(int)), key.(uuid.UUID).String())\n\t\treturn true\n\t})\n\n\tstarter.AddInstanceRetryCount.Range(func(key, value any) bool {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tmemoryRunnerCreateRetryCount, prometheus.GaugeValue, float64(value.(int)), key.(uuid.UUID).String())\n\t\treturn true\n\t})\n\n\treturn nil\n}\n\nfunc scrapeGitHubValues(ch chan<- prometheus.Metric) error {\n\trateLimitRemain := gh.GetRateLimitRemain()\n\tfor scope, remain := range rateLimitRemain {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tmemoryGitHubRateLimitRemaining, prometheus.GaugeValue, float64(remain), scope,\n\t\t)\n\t}\n\n\trateLimitLimit := gh.GetRateLimitLimit()\n\tfor scope, limit := range rateLimitLimit {\n\t\tch <- prometheus.MustNewConstMetric(\n\t\t\tmemoryGitHubRateLimitLimiting, prometheus.GaugeValue, float64(limit), scope,\n\t\t)\n\t}\n\n\treturn nil\n}\n\nvar _ Scraper = ScraperMemory{}\n\nfunc scrapeDockerValues(ch chan<- prometheus.Metric) error {\n\trateLimit, err := docker.GetRateLimit()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get rate limit: %w\", err)\n\t}\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryDockerHubRateLimitRemaining, prometheus.GaugeValue, float64(rateLimit.Remaining),\n\t)\n\tch <- prometheus.MustNewConstMetric(\n\t\tmemoryDockerHubRateLimitLimiting, prometheus.GaugeValue, float64(rateLimit.Limit),\n\t)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/metric/webhook.go",
    "content": "package metric\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\t// WebhookReceivedTotal is the total number of webhooks received\n\tWebhookReceivedTotal = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: \"webhook\",\n\t\t\tName:      \"received_total\",\n\t\t\tHelp:      \"Total number of webhooks received from GitHub\",\n\t\t},\n\t\t[]string{\"event_type\", \"status\", \"runs_on\"},\n\t)\n\n\t// WebhookProcessingDuration is the duration of webhook processing\n\tWebhookProcessingDuration = promauto.NewHistogramVec(\n\t\tprometheus.HistogramOpts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: \"webhook\",\n\t\t\tName:      \"processing_duration_seconds\",\n\t\t\tHelp:      \"Duration of webhook processing in seconds\",\n\t\t\tBuckets:   prometheus.DefBuckets,\n\t\t},\n\t\t[]string{\"event_type\", \"runs_on\"},\n\t)\n\n\t// WebhookJobsEnqueued is the total number of jobs enqueued from webhooks\n\tWebhookJobsEnqueued = promauto.NewCounterVec(\n\t\tprometheus.CounterOpts{\n\t\t\tNamespace: namespace,\n\t\t\tSubsystem: \"webhook\",\n\t\t\tName:      \"jobs_enqueued_total\",\n\t\t\tHelp:      \"Total number of jobs enqueued from webhooks\",\n\t\t},\n\t\t[]string{\"event_type\", \"repository\", \"runs_on\"},\n\t)\n)\n"
  },
  {
    "path": "pkg/runner/metrics.go",
    "content": "package runner\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\t// DeleteRunnerBackoffDuration is histogram of exponential backoff duration for deleting runner\n\tDeleteRunnerBackoffDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"myshoes\",\n\t\tSubsystem: \"runner\",\n\t\tName:      \"delete_runner_backoff_duration_seconds\",\n\t\tHelp:      \"Histogram of exponential backoff duration in seconds for deleting runner\",\n\t\tBuckets:   prometheus.ExponentialBuckets(1, 2, 10), // 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s\n\t}, []string{\"runner_uuid\"})\n\n\t// DeleteRunnerRetryTotal is counter of total retries for deleting runner\n\tDeleteRunnerRetryTotal = promauto.NewCounterVec(prometheus.CounterOpts{\n\t\tNamespace: \"myshoes\",\n\t\tSubsystem: \"runner\",\n\t\tName:      \"delete_runner_retry_total\",\n\t\tHelp:      \"Total number of retries for deleting runner\",\n\t}, []string{\"runner_uuid\"})\n)\n"
  },
  {
    "path": "pkg/runner/runner.go",
    "content": "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/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\nvar (\n\t// GoalCheckerInterval is interval time of check deleting runner\n\tGoalCheckerInterval = 1 * time.Minute\n\t// MustGoalTime is hard limit for idle runner.\n\t// So it is same as the limit of GitHub Actions\n\tMustGoalTime = 6 * time.Hour\n\t// MustRunningTime is set time of instance create + download binaries + etc\n\tMustRunningTime = 5 * time.Minute\n\t// TargetTokenInterval is interval time of checking target token\n\tTargetTokenInterval = 5 * time.Minute\n\t//NeedRefreshToken is time of token expired\n\tNeedRefreshToken = 10 * time.Minute\n)\n\n// Manager is runner management\ntype Manager struct {\n\tds            datastore.Datastore\n\trunnerVersion string\n}\n\n// New create a Manager\nfunc New(ds datastore.Datastore, runnerVersion string) *Manager {\n\treturn &Manager{\n\t\tds:            ds,\n\t\trunnerVersion: runnerVersion,\n\t}\n}\n\n// Loop check\nfunc (m *Manager) Loop(ctx context.Context) error {\n\tlogger.Logf(false, \"start runner loop\")\n\n\tticker := time.NewTicker(GoalCheckerInterval)\n\tdefer ticker.Stop()\n\n\tif err := m.doTargetToken(ctx); err != nil {\n\t\tlogger.Logf(false, \"failed to refresh token in initialize: %+v\", err)\n\t}\n\n\tgo func(ctx context.Context) {\n\t\ttokenRefreshTicker := time.NewTicker(TargetTokenInterval)\n\t\tdefer tokenRefreshTicker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-tokenRefreshTicker.C:\n\t\t\t\tif err := m.doTargetToken(ctx); err != nil {\n\t\t\t\t\tlogger.Logf(false, \"failed to refresh token: %+v\", err)\n\t\t\t\t}\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}(ctx)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tif err := m.do(ctx); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to starter: %+v\", err)\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\n// TemporaryMode is mode of temporary runner\ntype TemporaryMode int\n\n// RunnerEphemeralModes variable\nconst (\n\tTemporaryUnknown TemporaryMode = iota\n\tTemporaryOnce\n\tTemporaryEphemeral\n)\n\n// StringFlag return flag\nfunc (rtm TemporaryMode) StringFlag() string {\n\tswitch rtm {\n\tcase TemporaryOnce:\n\t\treturn \"--once\"\n\tcase TemporaryEphemeral:\n\t\treturn \"--ephemeral\"\n\t}\n\treturn \"unknown\"\n}\n\n// GetRunnerTemporaryMode get runner version and RunnerTemporaryMode\nfunc GetRunnerTemporaryMode(runnerVersion string) (string, TemporaryMode, error) {\n\tephemeralSupportVersion, err := version.NewVersion(\"v2.282.0\")\n\tif err != nil {\n\t\treturn \"\", TemporaryUnknown, fmt.Errorf(\"failed to parse ephemeral runner version: %w\", err)\n\t}\n\n\tinputVersion, err := version.NewVersion(runnerVersion)\n\tif err != nil {\n\t\treturn \"\", TemporaryUnknown, fmt.Errorf(\"failed to parse input runner version: %w\", err)\n\t}\n\n\tif ephemeralSupportVersion.GreaterThan(inputVersion) {\n\t\tlogger.Logf(false, \"WARNING: runner version is lower than v2.282.0, use --once mode. It's deprecated. will be removed in future.\")\n\t\treturn runnerVersion, TemporaryOnce, nil\n\t}\n\treturn runnerVersion, TemporaryEphemeral, nil\n}\n"
  },
  {
    "path": "pkg/runner/runner_delete.go",
    "content": "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-github/v80/github\"\n\t\"github.com/whywaita/myshoes/internal/util\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\t\"github.com/whywaita/myshoes/pkg/shoes\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/sync/semaphore\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\nvar (\n\t// ConcurrencyDeleting is value of concurrency\n\tConcurrencyDeleting atomic.Int64\n\t// DeletingTimeout is timeout of deleting runner\n\tDeletingTimeout = 3 * time.Minute\n\t// DeleteRetryCount is retry count of deleting runner\n\tDeleteRetryCount = sync.Map{} //  key: runner.UUID\n\t// MaxDeleteRetry is max retry count of delete runner\n\tMaxDeleteRetry = 10\n)\n\nfunc (m *Manager) do(ctx context.Context) error {\n\tlogger.Logf(true, \"start runner manager\")\n\n\ttargets, err := datastore.ListTargets(ctx, m.ds)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get targets: %w\", err)\n\t}\n\n\tlogger.Logf(true, \"found %d targets in datastore\", len(targets))\n\tfor _, target := range targets {\n\t\tlogger.Logf(true, \"start to search runner in %s\", target.Scope)\n\t\tif err := m.removeRunners(ctx, target); err != nil {\n\t\t\tlogger.Logf(false, \"failed to delete runners (target: %s): %+v\", target.Scope, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *Manager) removeRunners(ctx context.Context, t datastore.Target) error {\n\trunners, err := m.ds.ListRunnersByTargetID(ctx, t.UUID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to retrieve list of running runner: %w\", err)\n\t}\n\n\tvar mode TemporaryMode\n\tif strings.EqualFold(m.runnerVersion, \"latest\") {\n\t\tmode = TemporaryEphemeral\n\t} else {\n\t\t_, m, err := GetRunnerTemporaryMode(m.runnerVersion)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get runner mode: %w\", err)\n\t\t}\n\t\tmode = m\n\t}\n\n\tghRunners, err := isRegisteredRunnerZeroInGitHub(ctx, t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to check number of registerd runner: %w\", err)\n\t}\n\n\tif len(ghRunners) == 0 && len(runners) == 0 {\n\t\tswitch mode {\n\t\tcase TemporaryOnce:\n\t\t\tlogger.Logf(false, \"runner for queueing is not found in %s\", t.Scope)\n\t\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusErr, ErrDescriptionRunnerForQueueingIsNotFound); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t\t}\n\t\tdefault:\n\t\t\tif t.Status == datastore.TargetStatusErr && t.StatusDescription.Valid && strings.EqualFold(t.StatusDescription.String, ErrDescriptionRunnerForQueueingIsNotFound) {\n\t\t\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusActive, \"\"); err != nil {\n\t\t\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tsem := semaphore.NewWeighted(config.Config.MaxConcurrencyDeleting)\n\tvar eg errgroup.Group\n\tConcurrencyDeleting.Store(0)\n\n\tfor _, runner := range runners {\n\t\trunner := runner\n\t\tc, _ := DeleteRetryCount.LoadOrStore(runner.UUID, 0)\n\t\tcount, _ := c.(int)\n\t\tif count > MaxDeleteRetry {\n\t\t\tlogger.Logf(false, \"runner %s is retry count over %d, so will ignore\", runner.UUID, MaxDeleteRetry)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := sem.Acquire(ctx, 1); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to Acquire: %w\", err)\n\t\t}\n\t\tConcurrencyDeleting.Add(1)\n\n\t\teg.Go(func() error {\n\t\t\tcctx, cancel := context.WithTimeout(ctx, DeletingTimeout)\n\t\t\tdefer cancel()\n\n\t\t\tdefer func() {\n\t\t\t\tsem.Release(1)\n\t\t\t\tConcurrencyDeleting.Add(-1)\n\t\t\t}()\n\t\t\tsleep := util.CalcRetryTime(count)\n\t\t\tif count > 0 {\n\t\t\t\tDeleteRunnerRetryTotal.WithLabelValues(runner.UUID.String()).Inc()\n\t\t\t\tDeleteRunnerBackoffDuration.WithLabelValues(runner.UUID.String()).Observe(sleep.Seconds())\n\t\t\t}\n\t\t\ttime.Sleep(sleep)\n\n\t\t\tif err := m.removeRunner(cctx, t, runner, ghRunners); err != nil {\n\t\t\t\tDeleteRetryCount.Store(runner.UUID, count+1)\n\t\t\t\tlogger.Logf(false, \"failed to delete runner: %+v\", err)\n\t\t\t} else {\n\t\t\t\tDeleteRetryCount.Delete(runner.UUID)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := eg.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"failed to wait errgroup.Wait(): %w\", err)\n\t}\n\n\tif t.Status == datastore.TargetStatusRunning {\n\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusActive, \"\"); err != nil {\n\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *Manager) removeRunner(ctx context.Context, t datastore.Target, runner datastore.Runner, ghRunners []*github.Runner) error {\n\tif err := sanitizeRunnerMustRunningTime(runner); errors.Is(err, ErrNotWillDeleteRunner) {\n\t\tlogger.Logf(false, \"%s is not running MustRunningTime\", runner.UUID)\n\t\treturn nil\n\t}\n\tvar mode TemporaryMode\n\tif strings.EqualFold(m.runnerVersion, \"latest\") {\n\t\tmode = TemporaryEphemeral\n\t} else {\n\t\t_, m, err := GetRunnerTemporaryMode(m.runnerVersion)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get runner mode: %w\", err)\n\t\t}\n\t\tmode = m\n\t}\n\n\tswitch mode {\n\tcase TemporaryOnce:\n\t\tif err := m.removeRunnerModeOnce(ctx, t, runner, ghRunners); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove runner (mode once): %w\", err)\n\t\t}\n\tcase TemporaryEphemeral:\n\t\tif err := m.removeRunnerModeEphemeral(ctx, t, runner, ghRunners); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove runner (mode ephemeral): %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc isRegisteredRunnerZeroInGitHub(ctx context.Context, t datastore.Target) ([]*github.Runner, error) {\n\towner, repo := t.OwnerRepo()\n\tclient, err := gh.NewClient(t.GitHubToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create github client: %w\", err)\n\t}\n\n\tghRunners, err := gh.ListRunners(ctx, client, owner, repo)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get list of runner in GitHub: %w\", err)\n\t}\n\n\treturn ghRunners, nil\n}\n\nvar (\n\t// ErrNotWillDeleteRunner is error message for \"not will delete runner\"\n\tErrNotWillDeleteRunner = fmt.Errorf(\"not will delete runner\")\n)\n\nconst (\n\t// ErrDescriptionRunnerForQueueingIsNotFound is error message for datastore.StatusDescription \"runner for queueing is not found\"\n\tErrDescriptionRunnerForQueueingIsNotFound = \"runner for queueing is not found\"\n)\n\nvar (\n\t// StatusWillDelete will delete target in GitHub runners\n\tStatusWillDelete = \"offline\"\n\t// StatusSleep is sleeping runners\n\tStatusSleep = \"online\"\n)\n\nfunc sanitizeGitHubRunner(ghRunner github.Runner, dsRunner datastore.Runner) error {\n\tif ghRunner.GetBusy() {\n\t\t// runner is busy, so not will delete\n\t\treturn ErrNotWillDeleteRunner\n\t}\n\n\tswitch ghRunner.GetStatus() {\n\tcase StatusWillDelete:\n\t\tif err := sanitizeRunner(dsRunner, MustRunningTime); err != nil {\n\t\t\tlogger.Logf(false, \"%s is offline and not running %s, so not will delete (created_at: %s, now: %s)\", dsRunner.UUID, MustRunningTime, dsRunner.CreatedAt, time.Now().UTC())\n\t\t\treturn fmt.Errorf(\"failed to sanitize will delete runner: %w\", err)\n\t\t}\n\t\treturn nil\n\tcase StatusSleep:\n\t\tif err := sanitizeRunner(dsRunner, MustGoalTime); err != nil {\n\t\t\tlogger.Logf(false, \"%s is idle and not running %s, so not will delete (created_at: %s, now: %s)\", dsRunner.UUID, MustGoalTime, dsRunner.CreatedAt, time.Now().UTC())\n\t\t\treturn fmt.Errorf(\"failed to sanitize idle runner: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn ErrNotWillDeleteRunner\n}\n\nfunc sanitizeRunnerMustRunningTime(runner datastore.Runner) error {\n\treturn sanitizeRunner(runner, MustRunningTime)\n}\n\nfunc sanitizeRunner(runner datastore.Runner, needTime time.Duration) error {\n\tspent := runner.CreatedAt.Add(needTime)\n\tnow := time.Now().UTC()\n\tif !spent.Before(now) {\n\t\treturn ErrNotWillDeleteRunner\n\t}\n\n\treturn nil\n}\n\n// deleteRunnerWithGitHub delete runner in github, shoes, datastore.\n// runnerUUID is uuid in datastore, runnerID is id from GitHub.\nfunc (m *Manager) deleteRunnerWithGitHub(ctx context.Context, githubClient *github.Client, runner datastore.Runner, runnerID int64, owner, repo, runnerStatus string) error {\n\tlogger.Logf(false, \"will delete runner with GitHub: %s\", runner.UUID.String())\n\tisOrg := false\n\tif repo == \"\" {\n\t\tisOrg = true\n\t}\n\n\tif isOrg {\n\t\tif _, err := githubClient.Actions.RemoveOrganizationRunner(ctx, owner, runnerID); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove organization runner (runner uuid: %s): %+v\", runner.UUID.String(), err)\n\t\t}\n\t} else {\n\t\tif _, err := githubClient.Actions.RemoveRunner(ctx, owner, repo, runnerID); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove repository runner (runner uuid: %s): %+v\", runner.UUID.String(), err)\n\t\t}\n\t}\n\n\tif err := m.deleteRunner(ctx, runner, runnerStatus); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete runner: %w\", err)\n\t}\n\treturn nil\n}\n\n// deleteRunner delete runner in shoes, datastore.\nfunc (m *Manager) deleteRunner(ctx context.Context, runner datastore.Runner, runnerStatus string) error {\n\tlogger.Logf(false, \"will delete runner: %s\", runner.UUID.String())\n\n\tclient, teardown, err := shoes.GetClient()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get plugin client: %w\", err)\n\t}\n\tdefer teardown()\n\n\tlabels, err := gh.ExtractRunsOnLabels([]byte(runner.RequestWebhook))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to extract labels: %w\", err)\n\t}\n\n\tif err := client.DeleteInstance(ctx, runner.CloudID, labels); err != nil {\n\t\tif status.Code(errors.Unwrap(err)) == codes.NotFound {\n\t\t\tlogger.Logf(true, \"%s is not found, will ignore from shoes\", runner.UUID)\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"failed to delete instance: %w\", err)\n\t\t}\n\t}\n\n\tnow := time.Now().UTC()\n\tif err := m.ds.DeleteRunner(ctx, runner.UUID, now, ToReason(runnerStatus)); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove runner from datastore (runner uuid: %s): %+v\", runner.UUID.String(), err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/runner/runner_delete_ephemeral.go",
    "content": "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/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\n// removeRunnerModeEphemeral remove runner that created by --ephemeral flag.\n// --ephemeral flag is delete self-hosted runner when end of job. So, The origin list of runner from datastore.\nfunc (m *Manager) removeRunnerModeEphemeral(ctx context.Context, t datastore.Target, runner datastore.Runner, ghRunners []*github.Runner) error {\n\towner, repo := t.OwnerRepo()\n\tclient, err := gh.NewClient(t.GitHubToken)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create github client: %w\", err)\n\t}\n\n\tghRunner, err := gh.ExistGitHubRunnerWithRunner(ghRunners, ToName(runner.UUID.String()))\n\tswitch {\n\tcase errors.Is(err, gh.ErrNotFound):\n\t\t// deleted in GitHub, It's completed\n\t\tif err := m.deleteRunner(ctx, runner, StatusWillDelete); err != nil {\n\t\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusErr, \"\"); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"failed to delete runner: %w\", err)\n\t\t}\n\t\treturn nil\n\tcase err != nil:\n\t\treturn fmt.Errorf(\"failed to check runner exist in GitHub (runner: %s): %w\", runner.UUID, err)\n\t}\n\n\tif err := sanitizeGitHubRunner(*ghRunner, runner); err != nil {\n\t\tif errors.Is(err, ErrNotWillDeleteRunner) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to check runner of status: %w\", err)\n\t}\n\n\tif err := m.deleteRunnerWithGitHub(ctx, client, runner, ghRunner.GetID(), owner, repo, ghRunner.GetStatus()); err != nil {\n\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusErr, \"\"); err != nil {\n\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to delete runner with GitHub: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/runner/runner_delete_once.go",
    "content": "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/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\n// removeRunnerModeOnce remove runner that created by --once flag.\n// --once flag is not delete self-hosted runner when end of job. So, The origin list of runner from GitHub.\nfunc (m *Manager) removeRunnerModeOnce(ctx context.Context, t datastore.Target, runner datastore.Runner, ghRunners []*github.Runner) error {\n\towner, repo := t.OwnerRepo()\n\tclient, err := gh.NewClient(t.GitHubToken)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create github client: %w\", err)\n\t}\n\n\tghRunner, err := gh.ExistGitHubRunnerWithRunner(ghRunners, ToName(runner.UUID.String()))\n\tswitch {\n\tcase errors.Is(err, gh.ErrNotFound):\n\t\tlogger.Logf(false, \"NotFound in GitHub, so will delete in datastore without GitHub (runner: %s)\", runner.UUID.String())\n\t\tif err := m.deleteRunner(ctx, runner, StatusWillDelete); err != nil {\n\t\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusErr, \"\"); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"failed to delete runner: %w\", err)\n\t\t}\n\t\treturn nil\n\tcase err != nil:\n\t\treturn fmt.Errorf(\"failed to check runner exist in GitHub (runner: %s): %w\", runner.UUID, err)\n\t}\n\n\tif err := sanitizeGitHubRunner(*ghRunner, runner); err != nil {\n\t\tif errors.Is(err, ErrNotWillDeleteRunner) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to check runner of status: %w\", err)\n\t}\n\n\tif err := m.deleteRunnerWithGitHub(ctx, client, runner, ghRunner.GetID(), owner, repo, ghRunner.GetStatus()); err != nil {\n\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, t.UUID, datastore.TargetStatusErr, \"\"); err != nil {\n\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", t.UUID, err)\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to delete runner with GitHub: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/runner/token_update.go",
    "content": "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/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\nfunc (m *Manager) doTargetToken(ctx context.Context) error {\n\tlogger.Logf(true, \"start refresh token\")\n\n\ttargets, err := datastore.ListTargets(ctx, m.ds)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get targets: %w\", err)\n\t}\n\n\tfor _, target := range targets {\n\t\tneedRefreshTime := target.TokenExpiredAt.Add(-1 * NeedRefreshToken)\n\t\tif time.Now().Before(needRefreshTime) {\n\t\t\t// no need refresh\n\t\t\tcontinue\n\t\t}\n\n\t\t// do refresh\n\t\tlogger.Logf(true, \"%s need to update GitHub token, will be update\", target.UUID)\n\n\t\tclientApps, err := gh.NewClientGitHubApps()\n\t\tif err != nil {\n\t\t\tlogger.Logf(false, \"failed to create a client from Apps: %+v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tinstallationID, err := gh.IsInstalledGitHubApp(ctx, target.Scope)\n\t\tif err != nil {\n\t\t\tlogger.Logf(false, \"failed to get installationID: %+v\", err)\n\t\t\tcontinue\n\t\t}\n\t\t// TODO: replace to ghinstallation.AppTransport\n\t\ttoken, expiredAt, err := gh.GenerateGitHubAppsToken(ctx, clientApps, installationID, target.Scope)\n\t\tif err != nil {\n\t\t\tlogger.Logf(false, \"failed to get Apps Token: %+v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := m.ds.UpdateToken(ctx, target.UUID, token, *expiredAt); err != nil {\n\t\t\tlogger.Logf(false, \"failed to update token (target: %s): %+v\", target.UUID, err)\n\t\t\tif err := datastore.UpdateTargetStatus(ctx, m.ds, target.UUID, datastore.TargetStatusErr, \"can not update token\"); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to update target status (target ID: %s): %+v\\n\", target.UUID, err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/runner/util.go",
    "content": "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/datastore\"\n)\n\n// ToName convert uuid to runner name\nfunc ToName(u string) string {\n\treturn fmt.Sprintf(\"myshoes-%s\", u)\n}\n\n// ToUUID convert runner name to uuid\nfunc ToUUID(name string) (uuid.UUID, error) {\n\tu := strings.TrimPrefix(name, \"myshoes-\")\n\treturn uuid.FromString(u)\n}\n\n// ToReason convert status from GitHub to datastore.RunnerStatus\nfunc ToReason(status string) datastore.RunnerStatus {\n\tswitch status {\n\tcase StatusWillDelete:\n\t\t// is offline\n\t\treturn datastore.RunnerStatusCompleted\n\tcase StatusSleep:\n\t\t// is idle, reach hard limit\n\t\treturn datastore.RunnerStatusReachHardLimit\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/shoes/shoes.go",
    "content": "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/myshoes/api/proto.go\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n)\n\n// GetClient retrieve ShoesClient use shoes-plugin\nfunc GetClient() (Client, func(), error) {\n\tHandshake := plugin.HandshakeConfig{\n\t\tProtocolVersion:  1,\n\t\tMagicCookieKey:   \"SHOES_PLUGIN_MAGIC_COOKIE\",\n\t\tMagicCookieValue: \"are_you_a_shoes?\",\n\t}\n\tPluginMap := map[string]plugin.Plugin{\n\t\t\"shoes_grpc\": &Plugin{},\n\t}\n\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tHandshakeConfig:  Handshake,\n\t\tPlugins:          PluginMap,\n\t\tCmd:              exec.Command(config.Config.ShoesPluginPath),\n\t\tManaged:          true,\n\t\tStderr:           os.Stderr,\n\t\tSyncStdout:       os.Stdout,\n\t\tSyncStderr:       os.Stderr,\n\t\tAllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},\n\t})\n\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get shoes client: %w\", err)\n\t}\n\n\traw, err := rpcClient.Dispense(\"shoes_grpc\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to shoes client instance: %w\", err)\n\t}\n\n\treturn raw.(Client), client.Kill, nil\n}\n\n// Plugin is plugin implement\ntype Plugin struct {\n\tplugin.Plugin\n\n\tImpl Client\n}\n\n// GRPCServer is server\nfunc (p *Plugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {\n\treturn nil\n}\n\n// GRPCClient is client\nfunc (p *Plugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {\n\treturn &GRPCClient{client: pb.NewShoesClient(c)}, nil\n}\n\n// Client is plugin client interface\ntype Client interface {\n\tAddInstance(ctx context.Context, runnerID, setupScript string, resourceType datastore.ResourceType, labels []string) (string, string, string, datastore.ResourceType, error)\n\tDeleteInstance(ctx context.Context, cloudID string, labels []string) error\n}\n\n// GRPCClient is plugin client implement\ntype GRPCClient struct {\n\tclient pb.ShoesClient\n}\n\n// AddInstance create instance for runner\nfunc (c *GRPCClient) AddInstance(ctx context.Context, runnerName, setupScript string, resourceType datastore.ResourceType, labels []string) (string, string, string, datastore.ResourceType, error) {\n\treq := &pb.AddInstanceRequest{\n\t\tRunnerName:   runnerName,\n\t\tSetupScript:  setupScript,\n\t\tResourceType: resourceType.ToPb(),\n\t\tLabels:       labels,\n\t}\n\tresp, err := c.client.AddInstance(ctx, req)\n\tif err != nil {\n\t\t// will delete a job if labels of a job are invalid\n\t\tif stat, _ := status.FromError(err); stat.Code() == codes.InvalidArgument {\n\t\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, err\n\t\t}\n\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, fmt.Errorf(\"failed to AddInstance: %w\", err)\n\t}\n\n\treturn resp.CloudId, resp.IpAddress, resp.ShoesType, datastore.UnmarshalResourceType(resp.ResourceType), nil\n}\n\n// DeleteInstance delete instance for runner\nfunc (c *GRPCClient) DeleteInstance(ctx context.Context, cloudID string, labels []string) error {\n\treq := &pb.DeleteInstanceRequest{\n\t\tCloudId: cloudID,\n\t\tLabels:  labels,\n\t}\n\t_, err := c.client.DeleteInstance(ctx, req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to DeleteInstance: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/starter/README.md",
    "content": "## starter\n\nstarter is a dispatcher for running job"
  },
  {
    "path": "pkg/starter/error.go",
    "content": "package starter\n\nimport \"errors\"\n\ntype Error struct {\n\tkind internalError\n\terr  error\n}\n\nfunc (e Error) Error() string {\n\treturn e.kind.String() + \": \" + e.err.Error()\n}\n\nfunc (e Error) Unwrap() error {\n\treturn e.err\n}\n\ntype internalError int\n\nconst (\n\terrorInvalidLabel internalError = iota\n)\n\nfunc (i internalError) String() string {\n\tswitch i {\n\tcase errorInvalidLabel:\n\t\treturn \"invalid label\"\n\tdefault:\n\t\treturn \"unknown error\"\n\t}\n}\n\nvar (\n\tErrInvalidLabel = Error{kind: errorInvalidLabel, err: nil}\n)\n\nfunc NewInvalidLabel(err error) error {\n\te := ErrInvalidLabel\n\te.err = err\n\treturn e\n}\n\nfunc (e Error) Is(target error) bool {\n\tvar t Error\n\tok := errors.As(target, &t)\n\tif !ok {\n\t\treturn false\n\t}\n\treturn e.kind == t.kind\n}\n"
  },
  {
    "path": "pkg/starter/metric.go",
    "content": "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/gh\"\n)\n\nvar (\n\t// DeletedJobMap is map for deleted jobs. key: runs_on, value: number of deleted jobs\n\tDeletedJobMap = sync.Map{}\n)\n\nfunc incrementDeleteJobMap(j datastore.Job) error {\n\trunsOnConcat, err := gh.ConcatLabels(j.CheckEventJSON)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to concat labels: %+v\", err)\n\t}\n\tv, ok := DeletedJobMap.Load(runsOnConcat)\n\tif !ok {\n\t\tDeletedJobMap.Store(runsOnConcat, 1)\n\t\treturn nil\n\t}\n\n\tDeletedJobMap.Store(runsOnConcat, v.(int)+1)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/starter/metrics.go",
    "content": "package starter\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n\t// AddInstanceBackoffDuration is histogram of exponential backoff duration for adding instance\n\tAddInstanceBackoffDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{\n\t\tNamespace: \"myshoes\",\n\t\tSubsystem: \"starter\",\n\t\tName:      \"add_instance_backoff_duration_seconds\",\n\t\tHelp:      \"Histogram of exponential backoff duration in seconds for adding instance\",\n\t\tBuckets:   prometheus.ExponentialBuckets(1, 2, 10), // 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s\n\t}, []string{\"job_uuid\"})\n\n\t// AddInstanceRetryTotal is counter of total retries for adding instance\n\tAddInstanceRetryTotal = promauto.NewCounterVec(prometheus.CounterOpts{\n\t\tNamespace: \"myshoes\",\n\t\tSubsystem: \"starter\",\n\t\tName:      \"add_instance_retry_total\",\n\t\tHelp:      \"Total number of retries for adding instance\",\n\t}, []string{\"job_uuid\"})\n)\n"
  },
  {
    "path": "pkg/starter/safety/README.md",
    "content": "# safety\n\nsafety is interface of check to enable runner start."
  },
  {
    "path": "pkg/starter/safety/safety.go",
    "content": "package safety\n\nimport (\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n)\n\n// Safety is interface for safety\ntype Safety interface {\n\t// Check check that can create a runner. if can create a runner, return true.\n\tCheck(job *datastore.Job) (bool, error)\n}\n"
  },
  {
    "path": "pkg/starter/safety/unlimited/unlimited.go",
    "content": "package unlimited\n\nimport \"github.com/whywaita/myshoes/pkg/datastore\"\n\n// Unlimited is implement of safety.\n// Unlimited has not safety, so create a runner quickly.\ntype Unlimited struct{}\n\n// Check is not limited\nfunc (u Unlimited) Check(job *datastore.Job) (bool, error) {\n\treturn true, nil\n}\n"
  },
  {
    "path": "pkg/starter/scripts/RunnerService.js",
    "content": "#!/usr/bin/env node\n// Copyright (c) GitHub. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nvar childProcess = require(\"child_process\");\nvar path = require(\"path\")\n\nvar supported = ['linux', 'darwin']\n\nif (supported.indexOf(process.platform) == -1) {\n    console.log('Unsupported platform: ' + process.platform);\n    console.log('Supported platforms are: ' + supported.toString());\n    process.exit(1);\n}\n\nvar stopping = false;\nvar listener = null;\n\nvar runService = function() {\n    var listenerExePath = path.join(__dirname, '../bin/Runner.Listener');\n    var interactive = process.argv[2] === \"interactive\";\n\n    if(!stopping) {\n        try {\n            if (interactive) {\n                console.log('Starting Runner listener interactively');\n                listener = childProcess.spawn(listenerExePath, ['run'].concat(process.argv.slice(3)), { env: process.env });\n            } else {\n                console.log('Starting Runner listener with startup type: service');\n                listener = childProcess.spawn(listenerExePath, ['run', '--startuptype', 'service'].concat(process.argv.slice(2)), { env: process.env });\n            }\n\n            console.log('Started listener process');\n\n            listener.stdout.on('data', (data) => {\n                process.stdout.write(data.toString('utf8'));\n            });\n\n            listener.stderr.on('data', (data) => {\n                process.stdout.write(data.toString('utf8'));\n            });\n\n            listener.on('close', (code) => {\n                console.log(`Runner listener exited with error code ${code}`);\n\n                if (code === 0) {\n                    console.log('Runner listener exit with 0 return code, stop the service, no retry needed.');\n                    stopping = true;\n                } else if (code === 1) {\n                    console.log('Runner listener exit with terminated error, stop the service, no retry needed.');\n                    stopping = true;\n                } else if (code === 2) {\n                    console.log('Runner listener exit with retryable error, re-launch runner in 5 seconds.');\n                } else if (code === 3) {\n                    console.log('Runner listener exit because of updating, re-launch runner in 5 seconds.');\n                } else {\n                    console.log('Runner listener exit with undefined return code, re-launch runner in 5 seconds.');\n                }\n\n                if(!stopping) {\n                    setTimeout(runService, 5000);\n                }\n            });\n\n        } catch(ex) {\n            console.log(ex);\n        }\n    }\n}\n\nrunService();\nconsole.log('Started running service');\n\nvar gracefulShutdown = function(code) {\n    console.log('Shutting down runner listener');\n    stopping = true;\n    if (listener) {\n        console.log('Sending SIGINT to runner listener to stop');\n        listener.kill('SIGINT');\n\n        // TODO wait for 30 seconds and send a SIGKILL\n    }\n}\n\nprocess.on('SIGINT', () => {\n    gracefulShutdown(0);\n});\n\nprocess.on('SIGTERM', () => {\n    gracefulShutdown(0);\n});"
  },
  {
    "path": "pkg/starter/scripts.go",
    "content": "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\t\"text/template\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/runner\"\n)\n\n//go:embed scripts/RunnerService.js\nvar runnerService string\n\nfunc getPatchedFiles() (string, error) {\n\treturn runnerService, nil\n}\n\ntype templateCompressedScriptValue struct {\n\tCompressedScript    string\n\tRunnerBaseDirectory string\n}\n\nfunc (s *Starter) GetSetupScript(ctx context.Context, targetScope, runnerName string) (string, error) {\n\trawScript, err := s.getSetupRawScript(ctx, targetScope, runnerName)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get raw setup scripts: %w\", err)\n\t}\n\n\tvar compressedScript bytes.Buffer\n\tgz := gzip.NewWriter(&compressedScript)\n\tif _, err := gz.Write([]byte(rawScript)); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to compress gzip: %w\", err)\n\t}\n\tif err := gz.Flush(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to flush gzip: %w\", err)\n\t}\n\tif err := gz.Close(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to close gzip: %w\", err)\n\t}\n\tencoded := base64.StdEncoding.EncodeToString(compressedScript.Bytes())\n\n\tv := templateCompressedScriptValue{\n\t\tCompressedScript:    encoded,\n\t\tRunnerBaseDirectory: config.Config.RunnerBaseDirectory,\n\t}\n\n\tt, err := template.New(\"templateCompressedScript\").Parse(templateCompressedScript)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create template: %w\", err)\n\t}\n\tvar buff bytes.Buffer\n\tif err := t.Execute(&buff, v); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to execute compressed script: %w\", err)\n\t}\n\treturn buff.String(), nil\n}\n\nfunc (s *Starter) getSetupRawScript(ctx context.Context, targetScope, runnerName string) (string, error) {\n\trunnerUser := config.Config.RunnerUser\n\n\ttargetRunnerVersion := s.runnerVersion\n\tif strings.EqualFold(s.runnerVersion, \"latest\") {\n\t\tlatestVersion, err := gh.GetLatestRunnerVersion(ctx, targetScope)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get latest version of actions/runner: %w\", err)\n\t\t}\n\t\ttargetRunnerVersion = latestVersion\n\t}\n\n\trunnerVersion, runnerTemporaryMode, err := runner.GetRunnerTemporaryMode(targetRunnerVersion)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get runner version: %w\", err)\n\t}\n\n\trunnerServiceJs, err := getPatchedFiles()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get patched files: %w\", err)\n\t}\n\n\tinstallationID, err := gh.IsInstalledGitHubApp(ctx, targetScope)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get installlation id: %w\", err)\n\t}\n\ttoken, err := gh.GetRunnerRegistrationToken(ctx, installationID, targetScope)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate runner register token: %w\", err)\n\t}\n\n\tvar labels []string\n\t// The \"dependabot\" label is always added to ensure compatibility with Dependabot-related workflows.\n\tlabels = append(labels, \"dependabot\")\n\n\tv := templateCreateLatestRunnerOnceValue{\n\t\tScope:                   targetScope,\n\t\tGHEDomain:               config.Config.GitHubURL,\n\t\tRunnerRegistrationToken: token,\n\t\tRunnerName:              runnerName,\n\t\tRunnerUser:              runnerUser,\n\t\tRunnerVersion:           runnerVersion,\n\t\tRunnerServiceJS:         runnerServiceJs,\n\t\tRunnerArg:               runnerTemporaryMode.StringFlag(),\n\t\tAdditionalLabels:        labelsToOneLine(labels),\n\t\tRunnerBaseDirectory:     config.Config.RunnerBaseDirectory,\n\t}\n\n\tt, err := template.New(\"templateCreateLatestRunnerOnce\").Parse(templateCreateLatestRunnerOnce)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create template\")\n\t}\n\tvar buff bytes.Buffer\n\tif err := t.Execute(&buff, v); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to execute scripts: %w\", err)\n\t}\n\treturn buff.String(), nil\n}\n\nfunc labelsToOneLine(labels []string) string {\n\tif len(labels) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\",%s\", strings.Join(labels, \",\"))\n}\n\nconst templateCompressedScript = `#!/bin/bash\n\nset -e\n\n# main script compressed base64 and gzip\nexport COMPRESSED_SCRIPT={{.CompressedScript}}\nexport MAIN_SCRIPT_PATH={{.RunnerBaseDirectory}}/main.sh\n\necho ${COMPRESSED_SCRIPT} | base64 -d | gzip -d > ${MAIN_SCRIPT_PATH}\n\nchmod +x ${MAIN_SCRIPT_PATH}\nbash -c ${MAIN_SCRIPT_PATH}`\n\ntype templateCreateLatestRunnerOnceValue struct {\n\tScope                   string\n\tGHEDomain               string\n\tRunnerRegistrationToken string\n\tRunnerName              string\n\tRunnerUser              string\n\tRunnerVersion           string\n\tRunnerServiceJS         string\n\tRunnerArg               string\n\tAdditionalLabels        string\n\tRunnerBaseDirectory     string\n}\n\n// templateCreateLatestRunnerOnce is script template of setup runner.\n// need to set runnerUser if execute using root permission. (for example, use cloud-init)\n// original script: https://github.com/actions/runner/blob/80bf68db812beb298b7534012b261e6f222e004a/scripts/create-latest-svc.sh\nconst templateCreateLatestRunnerOnce = `#!/bin/bash\n\nset -e\n\nrunner_scope={{.Scope}}\nghe_hostname={{.GHEDomain}}\nrunner_name={{.RunnerName}}\nRUNNER_TOKEN={{.RunnerRegistrationToken}}\nRUNNER_USER={{.RunnerUser}}\nRUNNER_VERSION={{.RunnerVersion}}\nRUNNER_BASE_DIRECTORY={{.RunnerBaseDirectory}}\n\nsudo_prefix=\"\"\nif [ $(id -u) -eq 0 ]; then  # if root\nsudo_prefix=\"sudo -E -u ${RUNNER_USER} \"\nfi\n\necho \"Configuring runner @ ${runner_scope}\"\n\n#---------------------------------------\n# Validate Environment\n#---------------------------------------\nrunner_plat=linux\n[ ! -z \"$(which sw_vers)\" ] && runner_plat=osx;\n\nfunction fatal()\n{\n   echo \"error: $1\" >&2\n   exit 1\n}\n\nfunction configure_environment()\n{\n\texport HOME=\"/home/${RUNNER_USER}\"\n\tif [ \"${runner_plat}\" = \"osx\" ]; then\n\t\texport HOME=\"/Users/${RUNNER_USER}\"\n\tfi\n}\n\nfunction install_jq()\n{\n    echo \"jq is not installed, will be install jq.\"\n    if [ -e /etc/debian_version ] || [ -e /etc/debian_release ]; then\n        sudo apt-get update -y -qq\n        sudo apt-get install -y jq\n    elif [ -e /etc/redhat-release ]; then\n        sudo yum install -y jq\n    fi\n\n\tif [ \"${runner_plat}\" = \"osx\" ]; then\n\t\tbrew install jq\n\tfi\n}\n\nfunction install_docker()\n{\n\techo \"docker is not installed, will be install docker.\"\n\tif [ -e /etc/debian_version ] || [ -e /etc/debian_release ]; then\n\t\tsudo apt-get update -y -qq\n\t\tsudo apt-get install -y docker.io\n\tfi\n\n\tif [ \"${runner_plat}\" = \"osx\" ]; then\n\t\techo \"No install in macOS, It is same that GitHub-hosted\" \n\tfi\n}\n\nfunction get_runner_file_name()\n{\n    runner_version=$1\n    runner_plat=$2\n\n    trimmed_runner_version=$(echo ${RUNNER_VERSION:1})\n\n    if [ \"${runner_plat}\" = \"linux\" ]; then\n        echo \"actions-runner-${runner_plat}-x64-${trimmed_runner_version}.tar.gz\"\n    fi\n\n    if [ \"${runner_plat}\" = \"osx\" ]; then\n        runner_arch=x64\n        [ \"$(uname -m)\" = \"arm64\" ] && runner_arch=arm64;\n        echo \"actions-runner-${runner_plat}-${runner_arch}-${trimmed_runner_version}.tar.gz\"\n    fi\n}\n\nfunction download_runner()\n{\n    runner_version=$1\n    runner_file=$2\n\n    runner_url=\"https://github.com/actions/runner/releases/download/${runner_version}/${runner_file}\"\n\n    echo \"Downloading ${runner_version} for ${runner_plat} ...\"\n    echo $runner_url\n\n    curl -O -L ${runner_url}\n\n    ls -la *.tar.gz\n}\n\nfunction extract_runner()\n{\n\trunner_file=$1\n\trunner_user=$2\n\n\techo \"Extracting ${runner_file} to ./runner\"\n\n\ttar xzf \"./${runner_file}\" -C runner\n\n\t# export of pass\n\tif [ $(id -u) -eq 0 ]; then\n\tchown -R ${runner_user} ./runner\n\tfi\n}\n\nif [ -z \"${runner_scope}\" ]; then fatal \"supply scope as argument 1\"; fi\n\nwhich curl || fatal \"curl required.  Please install in PATH with apt-get, brew, etc\"\nwhich jq || install_jq\nwhich jq || fatal \"jq required.  Please install in PATH with apt-get, brew, etc\"\nwhich docker || install_docker\n\nconfigure_environment\n\ncd ${RUNNER_BASE_DIRECTORY}\n${sudo_prefix}mkdir -p runner\n\n#---------------------------------------\n# Download latest released and extract\n#---------------------------------------\necho\necho \"Downloading latest runner ...\"\n\nrunner_file=$(get_runner_file_name ${RUNNER_VERSION} ${runner_plat})\n\nif [ -f \"${RUNNER_BASE_DIRECTORY}/runner/config.sh\" ]; then\n    # already extracted\n    echo \"${RUNNER_BASE_DIRECTORY}/runner/config.sh exists. skipping download and extract.\"\nelif [ -f \"/usr/local/etc/runner-${RUNNER_VERSION}/config.sh\" ]; then\n    echo \"runner-${RUNNER_VERSION} cache is found. skipping download and extract.\"\n    rm -r ./runner\n    mv /usr/local/etc/runner-${RUNNER_VERSION} ./runner\nelif [ -f \"${runner_file}\" ]; then\n    echo \"${runner_file} exists. skipping download.\"\n    extract_runner ${runner_file} ${RUNNER_USER}\nelif [ -f \"/usr/local/etc/${runner_file}\" ]; then\n    echo \"${runner_file} cache is found. skipping download.\"\n    mv /usr/local/etc/${runner_file} ./\n    extract_runner ${runner_file} ${RUNNER_USER}\nelse\n    download_runner ${RUNNER_VERSION} ${runner_file}\n    extract_runner ${runner_file} ${RUNNER_USER}\nfi\n\ncd ${RUNNER_BASE_DIRECTORY}/runner\n\n#---------------------------------------\n# Unattend config\n#---------------------------------------\nrunner_url=\"https://github.com/${runner_scope}\"\nif [ -n \"${ghe_hostname}\" ]; then\n    runner_url=\"${ghe_hostname}/${runner_scope}\"\nfi\n\necho\necho \"Configuring ${runner_name} @ $runner_url\"\n{{ if eq .RunnerArg \"--once\" -}}\necho \"./config.sh --unattended --url $runner_url --token *** --name $runner_name --labels myshoes\"\n${sudo_prefix}bash -c \"source /etc/environment; ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name --labels myshoes{{.AdditionalLabels}}\"\n{{ else -}}\necho \"./config.sh --unattended --url $runner_url --token *** --name $runner_name --labels myshoes {{.RunnerArg}}\"\n${sudo_prefix}bash -c \"source /etc/environment; ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name --labels myshoes{{.AdditionalLabels}} {{.RunnerArg}}\"\n{{ end }}\n\n\n#---------------------------------------\n# patch once commands\n#---------------------------------------\necho \"apply patch file\"\ncat << EOF > ./bin/runsvc.sh\n#!/bin/bash\n\n# convert SIGTERM signal to SIGINT\n# for more info on how to propagate SIGTERM to a child process see: http://veithen.github.io/2014/11/16/sigterm-propagation.html\ntrap 'kill -INT \\$PID' TERM INT\n\nif [ -f \".path\" ]; then\n    # configure\n    export PATH=\\$(cat .path)\n    echo \".path=\\${PATH}\"\nfi\n\n# insert anything to setup env when running as a service\n\n# run the host process which keep the listener alive\nNODE_PATH=\"./externals/node20/bin/node\"\nif [ ! -e \"\\${NODE_PATH}\" ]; then\n  NODE_PATH=\"./externals/node16/bin/node\"\nfi\n\\${NODE_PATH} ./bin/RunnerService.js \\$* &\nPID=\\$!\nwait \\$PID\ntrap - TERM INT\nwait \\$PID\nEOF\n\ncat << 'EOF' > ./bin/RunnerService.js\n{{.RunnerServiceJS}}\nEOF\n\n#---------------------------------------\n# Configure run commands\n#---------------------------------------\n\n# Configure job management hooks if script files exist\nif [ -e \"/myshoes-actions-runner-hook-job-started.sh\" ]; then\n\texport ACTIONS_RUNNER_HOOK_JOB_STARTED=\"/myshoes-actions-runner-hook-job-started.sh\"\nfi\nif [ -e \"/myshoes-actions-runner-hook-job-completed.sh\" ]; then\n\texport ACTIONS_RUNNER_HOOK_JOB_COMPLETED=\"/myshoes-actions-runner-hook-job-completed.sh\"\nfi\n\n#---------------------------------------\n# run!\n#---------------------------------------\n\n# GitHub-hosted runner load /etc/environment in /opt/runner/provisioner/provisioner.\n# So, we need to load /etc/environment for job on self-hosted runner.\n\n{{ if eq .RunnerArg \"--once\" -}}\necho 'bash -c \"source /etc/environment; ./bin/runsvc.sh  {{.RunnerArg}}\"'\n${sudo_prefix}bash -c \"source /etc/environment; ./bin/runsvc.sh  {{.RunnerArg}}\"\n{{ else -}}\necho 'bash -c \"source /etc/environment; ./bin/runsvc.sh\"'\n${sudo_prefix}bash -c \"source /etc/environment; ./bin/runsvc.sh\"\n{{ end }}`\n"
  },
  {
    "path": "pkg/starter/starter.go",
    "content": "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\"\n\t\"time\"\n\n\t\"github.com/google/go-github/v80/github\"\n\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/sync/semaphore\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\tuuid \"github.com/satori/go.uuid\"\n\t\"github.com/whywaita/myshoes/internal/util\"\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\t\"github.com/whywaita/myshoes/pkg/runner\"\n\t\"github.com/whywaita/myshoes/pkg/shoes\"\n\t\"github.com/whywaita/myshoes/pkg/starter/safety\"\n)\n\nvar (\n\t// CountRunning is count of running semaphore\n\tCountRunning atomic.Int64\n\t// CountWaiting is count of waiting job\n\tCountWaiting atomic.Int64\n\n\t// CountRescued is count of rescued job per target\n\tCountRescued = sync.Map{}\n\n\tinProgress = sync.Map{}\n\n\t// AddInstanceRetryCount is count of retry to add instance\n\tAddInstanceRetryCount = sync.Map{}\n)\n\n// Starter is dispatcher for running job\ntype Starter struct {\n\tds              datastore.Datastore\n\tsafety          safety.Safety\n\trunnerVersion   string\n\tnotifyEnqueueCh <-chan struct{}\n}\n\n// New create starter instance\nfunc New(ds datastore.Datastore, s safety.Safety, runnerVersion string, notifyEnqueueCh <-chan struct{}) *Starter {\n\treturn &Starter{\n\t\tds:              ds,\n\t\tsafety:          s,\n\t\trunnerVersion:   runnerVersion,\n\t\tnotifyEnqueueCh: notifyEnqueueCh,\n\t}\n}\n\n// Loop is main loop for starter\nfunc (s *Starter) Loop(ctx context.Context) error {\n\tlogger.Logf(false, \"start starter loop\")\n\tch := make(chan datastore.Job)\n\n\teg, ctx := errgroup.WithContext(ctx)\n\n\teg.Go(func() error {\n\t\tticker := time.NewTicker(10 * time.Second)\n\t\tdefer ticker.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\ts.reRunWorkflow(ctx)\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t})\n\n\teg.Go(func() error {\n\t\tif err := s.run(ctx, ch); err != nil {\n\t\t\treturn fmt.Errorf(\"faied to start processor: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\n\teg.Go(func() error {\n\t\tticker := time.NewTicker(10 * time.Second)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tif err := s.dispatcher(ctx, ch); err != nil {\n\t\t\t\t\tlogger.Logf(false, \"failed to starter: %+v\", err)\n\t\t\t\t}\n\t\t\tcase <-s.notifyEnqueueCh:\n\t\t\t\tticker.Reset(10 * time.Second)\n\t\t\t\tif err := s.dispatcher(ctx, ch); err != nil {\n\t\t\t\t\tlogger.Logf(false, \"failed to starter: %+v\", err)\n\t\t\t\t}\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t})\n\n\tif err := eg.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"failed to errgroup wait: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (s *Starter) dispatcher(ctx context.Context, ch chan datastore.Job) error {\n\tlogger.Logf(true, \"start to check starter\")\n\tjobs, err := s.ds.ListJobs(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get jobs: %w\", err)\n\t}\n\n\tfor _, j := range jobs {\n\t\t// send to processor\n\t\tch <- j\n\t}\n\n\treturn nil\n}\n\nfunc (s *Starter) run(ctx context.Context, ch chan datastore.Job) error {\n\tsem := semaphore.NewWeighted(config.Config.MaxConnectionsToBackend)\n\n\t// Processor\n\tfor {\n\t\tselect {\n\t\tcase job := <-ch:\n\t\t\t// receive job from dispatcher\n\n\t\t\tif _, ok := inProgress.Load(job.UUID); ok {\n\t\t\t\t// this job is in progress, skip\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tc, _ := AddInstanceRetryCount.LoadOrStore(job.UUID, 0)\n\t\t\tcount, _ := c.(int)\n\n\t\t\trunID, jobID, err := extractWorkflowIDs(job)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Logf(true, \"found new job: %s (repo: %s)\", job.UUID, job.Repository)\n\t\t\t} else {\n\t\t\t\tlogger.Logf(true, \"found new job: %s (gh_run_id: %d, gh_job_id: %d, repo: %s)\", job.UUID, runID, jobID, job.Repository)\n\t\t\t}\n\t\t\tCountWaiting.Add(1)\n\t\t\tif err := sem.Acquire(ctx, 1); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to Acquire: %w\", err)\n\t\t\t}\n\t\t\tCountWaiting.Add(-1)\n\t\t\tCountRunning.Add(1)\n\n\t\t\tinProgress.Store(job.UUID, struct{}{})\n\n\t\t\tsleep := util.CalcRetryTime(count)\n\t\t\tif count > 0 {\n\t\t\t\tAddInstanceRetryTotal.WithLabelValues(job.UUID.String()).Inc()\n\t\t\t\tAddInstanceBackoffDuration.WithLabelValues(job.UUID.String()).Observe(sleep.Seconds())\n\t\t\t}\n\t\t\tgo func(job datastore.Job, sleep time.Duration) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tsem.Release(1)\n\t\t\t\t\tinProgress.Delete(job.UUID)\n\t\t\t\t\tCountRunning.Add(-1)\n\t\t\t\t}()\n\n\t\t\t\ttime.Sleep(sleep)\n\t\t\t\tif err := s.ProcessJob(ctx, job); err != nil {\n\t\t\t\t\tAddInstanceRetryCount.Store(job.UUID, count+1)\n\t\t\t\t\tlogger.Logf(false, \"failed to process job: %+v\\n\", err)\n\t\t\t\t} else {\n\t\t\t\t\tAddInstanceRetryCount.Delete(job.UUID)\n\t\t\t\t}\n\t\t\t}(job, sleep)\n\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\n// extractWorkflowIDs extracts GitHub workflow run ID and job ID from a datastore.Job\nfunc extractWorkflowIDs(job datastore.Job) (runID int64, jobID int64, err error) {\n\twebhookEvent, err := github.ParseWebHook(\"workflow_job\", []byte(job.CheckEventJSON))\n\tif err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"failed to parse webhook: %w\", err)\n\t}\n\n\tworkflowJob, ok := webhookEvent.(*github.WorkflowJobEvent)\n\tif !ok {\n\t\treturn 0, 0, fmt.Errorf(\"failed to cast to WorkflowJobEvent\")\n\t}\n\n\tif workflowJob.GetWorkflowJob() == nil {\n\t\treturn 0, 0, fmt.Errorf(\"workflow job is nil\")\n\t}\n\n\treturn workflowJob.GetWorkflowJob().GetRunID(), workflowJob.GetWorkflowJob().GetID(), nil\n}\n\n// ProcessJob is process job\nfunc (s *Starter) ProcessJob(ctx context.Context, job datastore.Job) error {\n\trunID, jobID, err := extractWorkflowIDs(job)\n\tif err != nil {\n\t\tlogger.Logf(false, \"start job (job id: %s, repo: %s)\\n\", job.UUID.String(), job.Repository)\n\t} else {\n\t\tlogger.Logf(false, \"start job (job id: %s, gh_run_id: %d, gh_job_id: %d, repo: %s)\\n\", job.UUID.String(), runID, jobID, job.Repository)\n\t}\n\n\tisOK, err := s.safety.Check(&job)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to check safety: %w\", err)\n\t}\n\tif !isOK {\n\t\t// is not ok, save job\n\t\treturn nil\n\t}\n\tif err := datastore.UpdateTargetStatus(ctx, s.ds, job.TargetID, datastore.TargetStatusRunning, \"\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to update target status (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t}\n\n\ttarget, err := s.ds.GetTarget(ctx, job.TargetID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to retrieve relational target: (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t}\n\n\tcctx, cancel := context.WithTimeout(ctx, runner.MustRunningTime)\n\tdefer cancel()\n\tcloudID, ipAddress, shoesType, resourceType, err := s.bung(cctx, job, *target)\n\tif err != nil {\n\t\trunID2, jobID2, extractErr := extractWorkflowIDs(job)\n\t\tif extractErr != nil {\n\t\t\tlogger.Logf(false, \"failed to bung (target ID: %s, job ID: %s): %+v\", job.TargetID, job.UUID, err)\n\t\t} else {\n\t\t\tlogger.Logf(false, \"failed to bung (target ID: %s, job ID: %s, gh_run_id: %d, gh_job_id: %d): %+v\", job.TargetID, job.UUID, runID2, jobID2, err)\n\t\t}\n\n\t\tif errors.Is(err, ErrInvalidLabel) {\n\t\t\tif extractErr != nil {\n\t\t\t\tlogger.Logf(false, \"invalid argument. so will delete (job ID: %s)\", job.UUID)\n\t\t\t} else {\n\t\t\t\tlogger.Logf(false, \"invalid argument. so will delete (job ID: %s, gh_run_id: %d, gh_job_id: %d)\", job.UUID, runID2, jobID2)\n\t\t\t}\n\t\t\tif err := s.ds.DeleteJob(ctx, job.UUID); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to delete job: %+v\\n\", err)\n\n\t\t\t\tif err := datastore.UpdateTargetStatus(ctx, s.ds, job.TargetID, datastore.TargetStatusErr, fmt.Sprintf(\"job id: %s\", job.UUID)); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to update target status (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t\t\t\t}\n\n\t\t\t\treturn fmt.Errorf(\"failed to delete job: %w\", err)\n\t\t\t}\n\t\t\tif err := incrementDeleteJobMap(job); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to increment delete metrics: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := datastore.UpdateTargetStatus(ctx, s.ds, job.TargetID, datastore.TargetStatusErr, fmt.Sprintf(\"failed to create an instance (job ID: %s)\", job.UUID)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update target status (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to bung (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t}\n\tif resourceType == datastore.ResourceTypeUnknown {\n\t\tresourceType = target.ResourceType\n\t}\n\n\trunnerName := runner.ToName(job.UUID.String())\n\tif config.Config.Strict {\n\t\tif err := s.checkRegisteredRunner(ctx, runnerName, *target); err != nil {\n\t\t\tlogger.Logf(false, \"failed to check to register runner (target ID: %s, job ID: %s): %+v\\n\", job.TargetID, job.UUID, err)\n\n\t\t\tif err := deleteInstance(ctx, cloudID, job.CheckEventJSON); err != nil {\n\t\t\t\tlogger.Logf(false, \"failed to delete an instance that not registered instance (target ID: %s, cloud ID: %s): %+v\\n\", job.TargetID, cloudID, err)\n\t\t\t\t// not return, need to update target status if err.\n\t\t\t}\n\n\t\t\tif err := datastore.UpdateTargetStatus(ctx, s.ds, job.TargetID, datastore.TargetStatusErr, fmt.Sprintf(\"cannot register runner to GitHub (job ID: %s)\", job.UUID)); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to update target status (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"failed to check to register runner (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t\t}\n\t}\n\n\tr := datastore.Runner{\n\t\tUUID:         job.UUID,\n\t\tShoesType:    shoesType,\n\t\tIPAddress:    ipAddress,\n\t\tTargetID:     job.TargetID,\n\t\tCloudID:      cloudID,\n\t\tResourceType: resourceType,\n\t\tRunnerUser: sql.NullString{\n\t\t\tString: config.Config.RunnerUser,\n\t\t\tValid:  true,\n\t\t},\n\t\tProviderURL:    target.ProviderURL,\n\t\tRepositoryURL:  job.RepoURL(),\n\t\tRequestWebhook: job.CheckEventJSON,\n\t}\n\tif err := s.ds.CreateRunner(ctx, r); err != nil {\n\t\tlogger.Logf(false, \"failed to save runner to datastore (target ID: %s, job ID: %s): %+v\\n\", job.TargetID, job.UUID, err)\n\n\t\tif err := datastore.UpdateTargetStatus(ctx, s.ds, job.TargetID, datastore.TargetStatusErr, fmt.Sprintf(\"job id: %s\", job.UUID)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update target status (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to save runner to datastore (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t}\n\n\tif err := s.ds.DeleteJob(ctx, job.UUID); err != nil {\n\t\tlogger.Logf(false, \"failed to delete job: %+v\\n\", err)\n\n\t\tif err := datastore.UpdateTargetStatus(ctx, s.ds, job.TargetID, datastore.TargetStatusErr, fmt.Sprintf(\"job id: %s\", job.UUID)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update target status (target ID: %s, job ID: %s): %w\", job.TargetID, job.UUID, err)\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to delete job: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// bung is start runner, like a pistol! :)\nfunc (s *Starter) bung(ctx context.Context, job datastore.Job, target datastore.Target) (string, string, string, datastore.ResourceType, error) {\n\trunID, jobID, extractErr := extractWorkflowIDs(job)\n\tif extractErr != nil {\n\t\tlogger.Logf(false, \"start create instance (job: %s)\", job.UUID)\n\t} else {\n\t\tlogger.Logf(false, \"start create instance (job: %s, gh_run_id: %d, gh_job_id: %d)\", job.UUID, runID, jobID)\n\t}\n\trunnerName := runner.ToName(job.UUID.String())\n\n\ttargetScope := getTargetScope(target, job)\n\tscript, err := s.GetSetupScript(ctx, targetScope, runnerName)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, fmt.Errorf(\"failed to get setup scripts: %w\", err)\n\t}\n\n\tclient, teardown, err := shoes.GetClient()\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, fmt.Errorf(\"failed to get plugin client: %w\", err)\n\t}\n\tdefer teardown()\n\n\tlabels, err := gh.ExtractRunsOnLabels([]byte(job.CheckEventJSON))\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, fmt.Errorf(\"failed to extract labels: %w\", err)\n\t}\n\n\tcloudID, ipAddress, shoesType, resourceType, err := client.AddInstance(ctx, runnerName, script, target.ResourceType, labels)\n\tif err != nil {\n\t\tif stat, _ := status.FromError(err); stat.Code() == codes.InvalidArgument {\n\t\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, NewInvalidLabel(err)\n\t\t}\n\t\treturn \"\", \"\", \"\", datastore.ResourceTypeUnknown, fmt.Errorf(\"failed to add instance: %w\", err)\n\t}\n\n\tif extractErr != nil {\n\t\tlogger.Logf(false, \"instance create successfully! (job: %s, cloud ID: %s)\", job.UUID, cloudID)\n\t} else {\n\t\tlogger.Logf(false, \"instance create successfully! (job: %s, cloud ID: %s, gh_run_id: %d, gh_job_id: %d)\", job.UUID, cloudID, runID, jobID)\n\t}\n\n\treturn cloudID, ipAddress, shoesType, resourceType, nil\n}\n\n// getTargetScope from target, but receive from job if datastore.target.Scope is empty\n// this function is for datastore that don't store target.\nfunc getTargetScope(target datastore.Target, job datastore.Job) string {\n\tif target.Scope == \"\" {\n\t\treturn job.Repository\n\t}\n\treturn target.Scope\n}\n\nfunc deleteInstance(ctx context.Context, cloudID, checkEventJSON string) error {\n\tclient, teardown, err := shoes.GetClient()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get plugin client: %w\", err)\n\t}\n\tdefer teardown()\n\n\tlabels, err := gh.ExtractRunsOnLabels([]byte(checkEventJSON))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to extract labels: %w\", err)\n\t}\n\n\tif err := client.DeleteInstance(ctx, cloudID, labels); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete instance: %w\", err)\n\t}\n\n\tlogger.Logf(false, \"successfully delete instance that not registered (cloud ID: %s)\", cloudID)\n\treturn nil\n}\n\n// checkRegisteredRunner check to register runner to GitHub\nfunc (s *Starter) checkRegisteredRunner(ctx context.Context, runnerName string, target datastore.Target) error {\n\tclient, err := gh.NewClient(target.GitHubToken)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create github client: %w\", err)\n\t}\n\towner, repo := gh.DivideScope(target.Scope)\n\n\tcctx, cancel := context.WithTimeout(ctx, runner.MustRunningTime)\n\tdefer cancel()\n\n\tticker := time.NewTicker(1 * time.Second)\n\tdefer ticker.Stop()\n\n\tcount := 0\n\tfor {\n\t\tselect {\n\t\tcase <-cctx.Done():\n\t\t\t// timeout\n\t\t\treturn fmt.Errorf(\"faied to to check existing runner in GitHub: timeout in %s\", runner.MustRunningTime)\n\t\tcase <-ticker.C:\n\t\t\tif _, err := gh.ExistGitHubRunner(cctx, client, owner, repo, runnerName); err == nil {\n\t\t\t\t// success to register runner to GitHub\n\t\t\t\treturn nil\n\t\t\t} else if !errors.Is(err, gh.ErrNotFound) {\n\t\t\t\t// not retryable error\n\t\t\t\treturn fmt.Errorf(\"failed to check existing runner in GitHub: %w\", err)\n\t\t\t}\n\t\t\tcount++\n\t\t\tlogger.Logf(true, \"%s is not found in GitHub, will retry... (second: %ds)\", runnerName, count)\n\t\t}\n\t}\n}\n\nfunc (s *Starter) reRunWorkflow(ctx context.Context) {\n\tpendingRuns, err := datastore.GetPendingWorkflowRunByRecentRepositories(ctx, s.ds)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to get pending workflow runs: %+v\", err)\n\t\treturn\n\t}\n\n\tfor _, pendingRun := range pendingRuns {\n\t\tif err := reRunWorkflowByPendingRun(ctx, s.ds, pendingRun); err != nil {\n\t\t\tlogger.Logf(false, \"failed to re-run workflow: %+v\", err)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc reRunWorkflowByPendingRun(ctx context.Context, ds datastore.Datastore, pendingRun datastore.PendingWorkflowRunWithTarget) error {\n\tif err := enqueueRescueRun(ctx, pendingRun, ds); err != nil {\n\t\treturn fmt.Errorf(\"failed to enqueue rescue job: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc enqueueRescueRun(ctx context.Context, pendingRun datastore.PendingWorkflowRunWithTarget, ds datastore.Datastore) error {\n\tfullName := pendingRun.WorkflowRun.GetRepository().GetFullName()\n\n\tclient, target, err := datastore.NewClientInstallationByRepo(ctx, ds, fullName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create a client of GitHub by repo (full_name: %s): %w\", fullName, err)\n\t}\n\n\tjobs, err := gh.ListWorkflowJobByRunID(\n\t\tctx,\n\t\tclient,\n\t\tpendingRun.WorkflowRun.GetRepository().GetOwner().GetLogin(),\n\t\tpendingRun.WorkflowRun.GetRepository().GetName(),\n\t\tpendingRun.WorkflowRun.GetID(),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list workflow jobs: %w\", err)\n\t}\n\n\tfor _, job := range jobs {\n\t\tif job.GetStatus() != \"queued\" && job.GetStatus() != \"pending\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if the job has appropriate labels for myshoes\n\t\tif !gh.IsRequestedMyshoesLabel(job.Labels) {\n\t\t\tlogger.Logf(true, \"skip rescue job because it doesn't have myshoes labels: (repo: %s, gh_run_id: %d, gh_job_id: %d, labels: %v)\",\n\t\t\t\tfullName, pendingRun.WorkflowRun.GetID(), job.GetID(), job.Labels)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Get installation ID from target scope\n\t\tinstallationID, err := gh.IsInstalledGitHubApp(ctx, target.Scope)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get installation ID: %w\", err)\n\t\t}\n\n\t\t// Get full installation data from cache\n\t\tinstallation, err := gh.GetInstallationByID(ctx, installationID)\n\t\tif err != nil {\n\t\t\tlogger.Logf(false, \"failed to get installation from cache (installationID: %d), using minimal data: %+v\", installationID, err)\n\t\t\t// Fallback to minimal installation data\n\t\t\tinstallation = &github.Installation{\n\t\t\t\tID: &installationID,\n\t\t\t}\n\t\t}\n\n\t\towner := pendingRun.WorkflowRun.GetRepository().GetOwner()\n\t\tvar org *github.Organization\n\t\tif owner != nil {\n\t\t\torg = &github.Organization{\n\t\t\t\tID:    owner.ID,\n\t\t\t\tLogin: owner.Login,\n\t\t\t\tName:  owner.Name,\n\t\t\t}\n\t\t}\n\n\t\tevent := &github.WorkflowJobEvent{\n\t\t\tWorkflowJob:  job,\n\t\t\tAction:       github.Ptr(\"queued\"),\n\t\t\tOrg:          org,\n\t\t\tRepo:         pendingRun.WorkflowRun.GetRepository(),\n\t\t\tSender:       pendingRun.WorkflowRun.GetActor(),\n\t\t\tInstallation: installation,\n\t\t}\n\n\t\tif err := enqueueRescueJob(ctx, event, *target, ds); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to enqueue rescue job: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc enqueueRescueJob(ctx context.Context, workflowJob *github.WorkflowJobEvent, target datastore.Target, ds datastore.Datastore) error {\n\tjobJSON, err := json.Marshal(workflowJob)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal job: %w\", err)\n\t}\n\n\trepository := workflowJob.GetRepo()\n\tif repository == nil {\n\t\treturn fmt.Errorf(\"repository is nil\")\n\t}\n\tfullName := repository.GetFullName()\n\tif fullName == \"\" {\n\t\treturn fmt.Errorf(\"repository full name is empty\")\n\t}\n\n\thtmlURL := repository.GetHTMLURL()\n\tif htmlURL == \"\" {\n\t\treturn fmt.Errorf(\"repository html url is empty\")\n\t}\n\tu, err := url.Parse(htmlURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse repository url from event: %w\", err)\n\t}\n\n\tgheDomain := \"\"\n\tif u.Host != \"github.com\" {\n\t\tgheDomain = fmt.Sprintf(\"%s://%s\", u.Scheme, u.Host)\n\t}\n\n\tlogger.Logf(false, \"rescue pending job: (repo: %s, gh_run_id: %d, gh_job_id: %d)\", *repository.HTMLURL, workflowJob.WorkflowJob.GetRunID(), workflowJob.WorkflowJob.GetID())\n\tjobID := uuid.NewV4()\n\tjob := datastore.Job{\n\t\tUUID: jobID,\n\t\tGHEDomain: sql.NullString{\n\t\t\tString: gheDomain,\n\t\t\tValid:  gheDomain != \"\",\n\t\t},\n\t\tRepository:     fullName,\n\t\tCheckEventJSON: string(jobJSON),\n\t\tTargetID:       target.UUID,\n\t}\n\tif err := ds.EnqueueJob(ctx, job); err != nil {\n\t\treturn fmt.Errorf(\"failed to enqueue job: %w\", err)\n\t}\n\n\t// Increment rescued runs counter\n\tv, _ := CountRescued.LoadOrStore(target.Scope, &atomic.Int64{})\n\tcounter := v.(*atomic.Int64)\n\tcounter.Add(1)\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/web/config.go",
    "content": "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/myshoes/pkg/logger\"\n)\n\ntype inputConfigDebug struct {\n\tDebug bool `json:\"debug\"`\n}\n\ntype inputConfigStrict struct {\n\tStrict bool `json:\"strict\"`\n}\n\nfunc handleConfigDebug(w http.ResponseWriter, r *http.Request) {\n\ti := inputConfigDebug{}\n\n\tif err := json.NewDecoder(r.Body).Decode(&i); err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"json decode error\")\n\t\treturn\n\t}\n\n\tconfig.Config.Debug = i.Debug\n\tlogger.Logf(false, \"switch debug mode to %t\", i.Debug)\n\tw.WriteHeader(http.StatusNoContent)\n}\n\nfunc handleConfigStrict(w http.ResponseWriter, r *http.Request) {\n\ti := inputConfigStrict{}\n\n\tif err := json.NewDecoder(r.Body).Decode(&i); err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"json decode error\")\n\t\treturn\n\t}\n\n\tconfig.Config.Strict = i.Strict\n\tlogger.Logf(false, \"switch strict mode to %t\", i.Strict)\n\tw.WriteHeader(http.StatusNoContent)\n}\n"
  },
  {
    "path": "pkg/web/http.go",
    "content": "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\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\n\tgoji \"goji.io\"\n\t\"goji.io/pat\"\n)\n\n// NewMux create routed mux\nfunc NewMux(ds datastore.Datastore) *goji.Mux {\n\tmux := goji.NewMux()\n\n\tmux.HandleFunc(pat.Get(\"/healthz\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\t\tw.WriteHeader(http.StatusOK)\n\n\t\th := struct {\n\t\t\tHealth string `json:\"health\"`\n\t\t}{\n\t\t\tHealth: \"ok\",\n\t\t}\n\n\t\tjson.NewEncoder(w).Encode(h)\n\t})\n\n\tmux.HandleFunc(pat.Post(\"/github/events\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\tHandleGitHubEvent(w, r, ds)\n\t})\n\n\t// REST API for targets\n\tmux.HandleFunc(pat.Post(\"/target\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleTargetCreate(w, r, ds)\n\t})\n\tmux.HandleFunc(pat.Get(\"/target\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleTargetList(w, r, ds)\n\t})\n\tmux.HandleFunc(pat.Get(\"/target/:id\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleTargetRead(w, r, ds)\n\t})\n\tmux.HandleFunc(pat.Post(\"/target/:id\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleTargetUpdate(w, r, ds)\n\t})\n\tmux.HandleFunc(pat.Delete(\"/target/:id\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleTargetDelete(w, r, ds)\n\t})\n\n\t// Config endpoints\n\tmux.HandleFunc(pat.Post(\"/config/debug\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleConfigDebug(w, r)\n\t})\n\tmux.HandleFunc(pat.Post(\"/config/strict\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\thandleConfigStrict(w, r)\n\t})\n\n\t// metrics endpoint\n\tmux.HandleFunc(pat.Get(\"/metrics\"), func(w http.ResponseWriter, r *http.Request) {\n\t\tapacheLogging(r)\n\t\tHandleMetrics(w, r, ds)\n\t})\n\n\treturn mux\n}\n\n// Serve start webhook receiver\nfunc Serve(ctx context.Context, ds datastore.Datastore) error {\n\tmux := NewMux(ds)\n\tlistenAddress := fmt.Sprintf(\":%d\", config.Config.Port)\n\ts := &http.Server{\n\t\tAddr:    listenAddress,\n\t\tHandler: mux,\n\t}\n\n\terrCh := make(chan error)\n\tgo func() {\n\t\tdefer close(errCh)\n\t\tlogger.Logf(false, \"start webhook receiver, listen %s\", listenAddress)\n\t\tif err := s.ListenAndServe(); err != nil {\n\t\t\terrCh <- fmt.Errorf(\"failed to listen and serve: %w\", err)\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn s.Shutdown(ctx)\n\tcase err := <-errCh:\n\t\treturn fmt.Errorf(\"occurred error in web serve: %w\", err)\n\t}\n}\n\nfunc apacheLogging(r *http.Request) {\n\tt := time.Now().UTC()\n\tlogger.Logf(false, \"HTTP - %s - - %s \\\"%s %s %s\\\"\\n\",\n\t\tr.RemoteAddr,\n\t\tt.Format(\"02/Jan/2006:15:04:05 -0700\"),\n\t\tr.Method,\n\t\tr.URL.Path,\n\t\tr.Proto,\n\t\t//interceptor.HTTPStatus,\n\t\t//interceptor.ResponseSize,\n\t\t//r.UserAgent(),\n\t\t//time.Since(t),\n\t)\n}\n"
  },
  {
    "path": "pkg/web/http_test.go",
    "content": "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 *testing.M) {\n\tos.Exit(testutils.IntegrationTestRunner(m))\n}\n"
  },
  {
    "path": "pkg/web/metrics.go",
    "content": "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/metric\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\n// HandleMetrics handle metrics endpoint\nfunc HandleMetrics(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\tctx := r.Context()\n\n\tregistry := prometheus.NewRegistry()\n\tregistry.MustRegister(metric.NewCollector(ctx, ds))\n\n\tgatherers := prometheus.Gatherers{\n\t\tprometheus.DefaultGatherer,\n\t\tregistry,\n\t}\n\th := promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{})\n\th.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "pkg/web/target.go",
    "content": "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/r3labs/diff/v2\"\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\n\t\"goji.io/pat\"\n)\n\n// TargetCreateParam is parameter for POST /target\ntype TargetCreateParam struct {\n\tdatastore.Target\n\n\tGHEDomain   *string `json:\"ghe_domain\"`   // ignore\n\tRunnerUser  *string `json:\"runner_user\"`  // nullable\n\tProviderURL *string `json:\"provider_url\"` // nullable\n}\n\n// UserTarget is format for user\ntype UserTarget struct {\n\tUUID              uuid.UUID              `json:\"id\"`\n\tScope             string                 `json:\"scope\"`\n\tTokenExpiredAt    time.Time              `json:\"token_expired_at\"`\n\tResourceType      string                 `json:\"resource_type\"`\n\tProviderURL       string                 `json:\"provider_url\"`\n\tStatus            datastore.TargetStatus `json:\"status\"`\n\tStatusDescription string                 `json:\"status_description\"`\n\tCreatedAt         time.Time              `json:\"created_at\"`\n\tUpdatedAt         time.Time              `json:\"updated_at\"`\n}\n\nfunc sortUserTarget(uts []UserTarget) []UserTarget {\n\tsort.SliceStable(uts, func(i, j int) bool {\n\t\tif uts[i].CreatedAt != uts[j].CreatedAt {\n\t\t\treturn uts[i].CreatedAt.After(uts[j].CreatedAt)\n\t\t}\n\n\t\tiType := datastore.UnmarshalResourceTypeString(uts[i].ResourceType)\n\t\tjType := datastore.UnmarshalResourceTypeString(uts[j].ResourceType)\n\n\t\treturn iType < jType\n\t})\n\n\treturn uts\n}\n\n// function pointer (for testing)\nvar (\n\tGHExistGitHubRepositoryFunc = gh.ExistGitHubRepository\n\tGHExistRunnerReleases       = gh.ExistRunnerReleases\n\tGHListRunnersFunc           = gh.ListRunners\n\tGHIsInstalledGitHubApp      = gh.IsInstalledGitHubApp\n\tGHGenerateGitHubAppsToken   = gh.GenerateGitHubAppsToken\n\tGHNewClientApps             = gh.NewClientGitHubApps\n\tGHPurgeInstallationCache    = gh.PurgeInstallationCache\n)\n\nfunc handleTargetList(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\tctx := r.Context()\n\n\tts, err := datastore.ListTargets(ctx, ds)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to retrieve list of target: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore read error\")\n\t}\n\n\tfmt.Println(ts)\n\tvar targets []UserTarget\n\tfor _, t := range ts {\n\t\tut := sanitizeTarget(t)\n\t\ttargets = append(targets, ut)\n\t}\n\n\ttargets = sortUserTarget(targets)\n\n\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\tw.WriteHeader(http.StatusOK)\n\tjson.NewEncoder(w).Encode(targets)\n}\n\nfunc handleTargetRead(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\tctx := r.Context()\n\ttargetID, err := parseReqTargetID(r)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"incorrect target id\")\n\t\treturn\n\t}\n\n\ttarget, err := ds.GetTarget(ctx, targetID)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to retrieve target from datastore: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore read error\")\n\t\treturn\n\t}\n\n\tut := sanitizeTarget(*target)\n\n\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\tw.WriteHeader(http.StatusOK)\n\tjson.NewEncoder(w).Encode(ut)\n}\n\nfunc sanitizeTarget(t datastore.Target) UserTarget {\n\tut := UserTarget{\n\t\tUUID:              t.UUID,\n\t\tScope:             t.Scope,\n\t\tTokenExpiredAt:    t.TokenExpiredAt,\n\t\tResourceType:      t.ResourceType.String(),\n\t\tProviderURL:       t.ProviderURL.String,\n\t\tStatus:            t.Status,\n\t\tStatusDescription: t.StatusDescription.String,\n\t\tCreatedAt:         t.CreatedAt,\n\t\tUpdatedAt:         t.UpdatedAt,\n\t}\n\n\treturn ut\n}\n\nfunc handleTargetUpdate(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\tctx := r.Context()\n\ttargetID, err := parseReqTargetID(r)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"incorrect target id\")\n\t\treturn\n\t}\n\n\tinputTarget := TargetCreateParam{}\n\tif err := json.NewDecoder(r.Body).Decode(&inputTarget); err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"json decode error\")\n\t\treturn\n\t}\n\tnewTarget := inputTarget.ToDS(\"\", time.Time{})\n\n\toldTarget, err := ds.GetTarget(ctx, targetID)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to get target: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"incorrect target id (not found)\")\n\t\treturn\n\t}\n\tif err := validateUpdateTarget(*oldTarget, newTarget); err != nil {\n\t\tlogger.Logf(false, \"input error in validateUpdateTarget: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\n\tresourceType, providerURL := getWillUpdateTargetVariable(getWillUpdateTargetVariableOld{\n\t\tresourceType: oldTarget.ResourceType,\n\t\tproviderURL:  oldTarget.ProviderURL,\n\t}, getWillUpdateTargetVariableNew{\n\t\tresourceType: inputTarget.ResourceType,\n\t\tproviderURL:  inputTarget.ProviderURL,\n\t})\n\tif err := ds.UpdateTargetParam(ctx, targetID, resourceType, providerURL); err != nil {\n\t\tlogger.Logf(false, \"failed to ds.UpdateTargetParam: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore update error\")\n\t\treturn\n\t}\n\n\tupdatedTarget, err := ds.GetTarget(ctx, targetID)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to get recently target in datastore: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore get error\")\n\t\treturn\n\t}\n\tut := sanitizeTarget(*updatedTarget)\n\n\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\tw.WriteHeader(http.StatusOK)\n\tjson.NewEncoder(w).Encode(ut)\n}\n\nfunc handleTargetDelete(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\tctx := r.Context()\n\ttargetID, err := parseReqTargetID(r)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"incorrect target id\")\n\t\treturn\n\t}\n\n\ttarget, err := ds.GetTarget(ctx, targetID)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to get target: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"incorrect target id (not found)\")\n\t\treturn\n\t}\n\tswitch target.Status {\n\tcase datastore.TargetStatusRunning:\n\t\tlogger.Logf(true, \"%s is running now\", targetID)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"target has running runner now, please stop all runner\")\n\t\treturn\n\tcase datastore.TargetStatusDeleted:\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"target is already deleted\")\n\t\treturn\n\t}\n\n\tif err := ds.DeleteTarget(ctx, targetID); err != nil {\n\t\tlogger.Logf(false, \"failed to delete target in datastore: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore delete error\")\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\tw.WriteHeader(http.StatusNoContent)\n}\n\nfunc parseReqTargetID(r *http.Request) (uuid.UUID, error) {\n\ttargetIDStr := pat.Param(r, \"id\")\n\ttargetID, err := uuid.FromString(targetIDStr)\n\tif err != nil {\n\t\treturn uuid.UUID{}, fmt.Errorf(\"failed to parse target id: %w\", err)\n\t}\n\n\treturn targetID, nil\n}\n\n// ErrorResponse is error response\ntype ErrorResponse struct {\n\tError string `json:\"error\"`\n}\n\nfunc outputErrorMsg(w http.ResponseWriter, status int, msg string) {\n\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\n\tw.WriteHeader(status)\n\n\tjson.NewEncoder(w).Encode(ErrorResponse{Error: msg})\n}\n\n// validateUpdateTarget check input target that can valid input in update.\nfunc validateUpdateTarget(old, new datastore.Target) error {\n\toldv := old\n\tnewv := new\n\n\tfor _, t := range []*datastore.Target{&oldv, &newv} {\n\t\tt.UUID = uuid.UUID{}\n\n\t\t// can update variables\n\t\tt.ResourceType = datastore.ResourceTypeUnknown\n\t\tt.ProviderURL = sql.NullString{}\n\n\t\t// time\n\t\tt.TokenExpiredAt = time.Time{}\n\t\tt.CreatedAt = time.Time{}\n\t\tt.UpdatedAt = time.Time{}\n\n\t\t// generated\n\t\tt.Status = \"\"\n\t\tt.StatusDescription = sql.NullString{}\n\t\tt.GitHubToken = \"\"\n\t}\n\n\tchangelog, err := diff.Diff(oldv, newv)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to check diff: %+v\", err)\n\t\treturn fmt.Errorf(\"failed to check diff: %w\", err)\n\t}\n\tif len(changelog) != 0 {\n\t\tlogger.Logf(false, \"invalid updatable parameter: %+v\", changelog)\n\n\t\tvar invalidFields []string\n\t\tfor _, cl := range changelog {\n\t\t\tif len(cl.Path) == 2 && !strings.EqualFold(cl.Path[1], \"String\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfieldName := cl.Path[0]\n\t\t\tinvalidFields = append(invalidFields, fieldName)\n\t\t}\n\n\t\treturn fmt.Errorf(\"invalid input: can't updatable fields (%s)\", strings.Join(invalidFields, \", \"))\n\t}\n\n\treturn nil\n}\n\nfunc isValidTargetCreateParam(input TargetCreateParam) error {\n\tif input.Scope == \"\" || input.ResourceType == datastore.ResourceTypeUnknown {\n\t\treturn fmt.Errorf(\"scope, resource_type must be set\")\n\t}\n\n\treturn nil\n}\n\nfunc toNullString(input *string) sql.NullString {\n\tif input == nil || strings.EqualFold(*input, \"\") {\n\t\treturn sql.NullString{\n\t\t\tValid: false,\n\t\t}\n\t}\n\n\treturn sql.NullString{\n\t\tValid:  true,\n\t\tString: *input,\n\t}\n}\n\n// ToDS convert to datastore.Target\nfunc (t *TargetCreateParam) ToDS(appToken string, tokenExpired time.Time) datastore.Target {\n\tproviderURL := toNullString(t.ProviderURL)\n\n\treturn datastore.Target{\n\t\tUUID:           t.UUID,\n\t\tScope:          t.Scope,\n\t\tGitHubToken:    appToken,\n\t\tTokenExpiredAt: tokenExpired,\n\t\tResourceType:   t.ResourceType,\n\t\tProviderURL:    providerURL,\n\t}\n}\n\ntype getWillUpdateTargetVariableOld struct {\n\tresourceType datastore.ResourceType\n\tproviderURL  sql.NullString\n}\n\ntype getWillUpdateTargetVariableNew struct {\n\tresourceType datastore.ResourceType\n\tproviderURL  *string\n}\n\nfunc getWillUpdateTargetVariable(oldParam getWillUpdateTargetVariableOld, newParam getWillUpdateTargetVariableNew) (datastore.ResourceType, sql.NullString) {\n\trt := oldParam.resourceType\n\tif newParam.resourceType != datastore.ResourceTypeUnknown {\n\t\trt = newParam.resourceType\n\t}\n\n\tproviderURL := getWillUpdateTargetVariableString(oldParam.providerURL, newParam.providerURL)\n\n\treturn rt, providerURL\n}\n\nfunc getWillUpdateTargetVariableString(old sql.NullString, new *string) sql.NullString {\n\tif new == nil {\n\t\treturn old\n\t}\n\treturn toNullString(new)\n}\n"
  },
  {
    "path": "pkg/web/target_create.go",
    "content": "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.com/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n)\n\nfunc handleTargetCreate(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\t// input values: scope, gpt, resource_type\n\tctx := r.Context()\n\tinputTarget := TargetCreateParam{}\n\tif err := json.NewDecoder(r.Body).Decode(&inputTarget); err != nil {\n\t\tlogger.Logf(false, \"failed to decode request body: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"json decode error\")\n\t\treturn\n\t}\n\n\tif err := isValidTargetCreateParam(inputTarget); err != nil {\n\t\tlogger.Logf(false, \"failed to validate input: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\tif err := GHPurgeInstallationCache(ctx); err != nil {\n\t\tlogger.Logf(false, \"failed to purge installation cache: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"failed to purge installation cache\")\n\t\treturn\n\t}\n\n\tinstallationID, err := GHIsInstalledGitHubApp(ctx, inputTarget.Scope)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to check installed GitHub App: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusBadRequest, \"failed to check to install GitHub Apps. Are you installed?\")\n\t\treturn\n\t}\n\n\tclientApps, err := GHNewClientApps()\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to client of GitHub Apps: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"failed to client GitHub Apps\")\n\t\treturn\n\t}\n\ttoken, expiredAt, err := GHGenerateGitHubAppsToken(ctx, clientApps, installationID, inputTarget.Scope)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to generate GitHub Apps Token: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"failed to generate GitHub Apps token\")\n\t\treturn\n\t}\n\n\tt := inputTarget.ToDS(token, *expiredAt)\n\tif err := isValidScopeAndToken(ctx, t.Scope, token); err != nil {\n\t\toutputErrorMsg(w, http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\n\ttarget, err := ds.GetTargetByScope(ctx, t.Scope)\n\tvar targetUUID uuid.UUID\n\n\tswitch {\n\tcase errors.Is(err, datastore.ErrNotFound):\n\t\t// not created, will be creating\n\t\tu, err := createNewTarget(ctx, t, ds)\n\t\tif err != nil {\n\t\t\toutputErrorMsg(w, http.StatusInternalServerError, err.Error())\n\t\t\treturn\n\t\t}\n\t\ttargetUUID = *u\n\tcase err != nil:\n\t\tlogger.Logf(false, \"failed to get target by scope [ghe_domain: %s scope: %s]: %+v\", config.Config.GitHubURL, t.Scope, err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore error\")\n\t\treturn\n\n\tcase target.Status != datastore.TargetStatusDeleted:\n\t\t// already registered\n\t\terrMsg := fmt.Sprintf(\"%s is already registered, current status is %s.\", t.Scope, target.Status)\n\t\toutputErrorMsg(w, http.StatusBadRequest, errMsg)\n\t\treturn\n\tcase target.Status == datastore.TargetStatusDeleted:\n\t\t// deleted, need to recreate\n\t\t//lint:ignore SA1019 ds.UpdateTargetStatus only use under.\n\t\tif err := ds.UpdateTargetStatus(ctx, target.UUID, datastore.TargetStatusActive, \"\"); err != nil {\n\t\t\tlogger.Logf(false, \"failed to recreate target: %+v\", err)\n\t\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore recreate error\")\n\t\t\treturn\n\t\t}\n\t\tresourceType, providerURL := getWillUpdateTargetVariable(getWillUpdateTargetVariableOld{\n\t\t\tresourceType: target.ResourceType,\n\t\t\tproviderURL:  target.ProviderURL,\n\t\t}, getWillUpdateTargetVariableNew{\n\t\t\tresourceType: inputTarget.ResourceType,\n\t\t\tproviderURL:  inputTarget.ProviderURL,\n\t\t})\n\t\tif err := ds.UpdateTargetParam(ctx, target.UUID, resourceType, providerURL); err != nil {\n\t\t\tlogger.Logf(false, \"failed to update resource type in recreating target: %+v\", err)\n\t\t\toutputErrorMsg(w, http.StatusInternalServerError, \"update resource type error\")\n\t\t\treturn\n\t\t}\n\n\t\ttargetUUID = target.UUID\n\t}\n\n\tcreatedTarget, err := ds.GetTarget(ctx, targetUUID)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to get recently target in datastore: %+v\", err)\n\t\toutputErrorMsg(w, http.StatusInternalServerError, \"datastore get error\")\n\t\treturn\n\t}\n\tut := sanitizeTarget(*createdTarget)\n\n\tw.Header().Set(\"Content-Type\", \"application/json;charset=utf-8\")\n\tw.WriteHeader(http.StatusCreated)\n\tjson.NewEncoder(w).Encode(ut)\n}\n\nfunc isValidScopeAndToken(ctx context.Context, scope, githubPersonalToken string) error {\n\tif err := GHExistGitHubRepositoryFunc(scope, githubPersonalToken); err != nil {\n\t\tlogger.Logf(false, \"failed to found github repository: %+v\", err)\n\t\treturn fmt.Errorf(\"github scope is invalid (maybe, repository is not found)\")\n\t}\n\n\tclient, err := gh.NewClient(githubPersonalToken)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to create GitHub client: %+v\", err)\n\t\treturn fmt.Errorf(\"invalid github token in input scope\")\n\t}\n\towner, repo := gh.DivideScope(scope)\n\tif _, err := GHListRunnersFunc(ctx, client, owner, repo); err != nil {\n\t\tlogger.Logf(false, \"failed to get list of registered runners: %+v\", err)\n\t\treturn fmt.Errorf(\"failed to get list of registered runners (maybe, invalid scope or token?)\")\n\t}\n\n\treturn nil\n}\n\nfunc createNewTarget(ctx context.Context, input datastore.Target, ds datastore.Datastore) (*uuid.UUID, error) {\n\tinput.UUID = uuid.NewV4()\n\tnow := time.Now().UTC()\n\tinput.CreatedAt = now\n\tinput.UpdatedAt = now\n\n\tinput.GHEDomain = sql.NullString{}\n\tif config.Config.GitHubURL != \"https://github.com\" {\n\t\tinput.GHEDomain = sql.NullString{\n\t\t\tString: config.Config.GitHubURL,\n\t\t\tValid:  true,\n\t\t}\n\t}\n\tif err := ds.CreateTarget(ctx, input); err != nil {\n\t\tlogger.Logf(false, \"failed to create target in datastore: %+v\", err)\n\t\treturn nil, fmt.Errorf(\"datastore create error\")\n\t}\n\n\treturn &input.UUID, nil\n}\n"
  },
  {
    "path": "pkg/web/target_test.go",
    "content": "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\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-github/v80/github\"\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/internal/testutils\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/web\"\n)\n\nvar testInstallationID = int64(100000000)\nvar testGitHubAppToken = \"secret-app-token\"\nvar testTime = time.Date(2037, 9, 3, 0, 0, 0, 0, time.UTC)\n\nfunc parseResponse(resp *http.Response) ([]byte, int) {\n\tdefer resp.Body.Close()\n\tcontent, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn content, resp.StatusCode\n}\n\nfunc setStubFunctions() {\n\tweb.GHExistGitHubRepositoryFunc = func(scope string, githubPersonalToken string) error {\n\t\treturn nil\n\t}\n\n\tweb.GHExistRunnerReleases = func(runnerVersion string) error {\n\t\treturn nil\n\t}\n\n\tweb.GHListRunnersFunc = func(ctx context.Context, client *github.Client, owner, repo string) ([]*github.Runner, error) {\n\t\treturn nil, nil\n\t}\n\n\tweb.GHIsInstalledGitHubApp = func(ctx context.Context, inputScope string) (int64, error) {\n\t\treturn testInstallationID, nil\n\t}\n\n\tweb.GHGenerateGitHubAppsToken = func(ctx context.Context, clientInstallation *github.Client, installationID int64, scope string) (string, *time.Time, error) {\n\t\treturn testGitHubAppToken, &testTime, nil\n\t}\n\n\tweb.GHNewClientApps = func() (*github.Client, error) {\n\t\treturn &github.Client{}, nil\n\t}\n\n\tweb.GHPurgeInstallationCache = func(ctx context.Context) error {\n\t\treturn nil\n\t}\n}\n\nfunc Test_handleTargetCreate(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\t_, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\ttests := []struct {\n\t\tinput          string\n\t\tinputGHEDomain string\n\t\twant           *web.UserTarget\n\t\terr            bool\n\t}{\n\t\t{\n\t\t\tinput: `{\"scope\": \"octocat\", \"resource_type\": \"micro\", \"runner_user\": \"runner\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tScope:          \"octocat\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeMicro.String(),\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t\terr: false,\n\t\t},\n\t\t{\n\t\t\tinput: `{\"scope\": \"whywaita/whywaita\", \"resource_type\": \"nano\", \"runner_user\": \"runner\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tScope:          \"whywaita/whywaita\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t\t{ // Confirm that no error occurs even if ghe_domain is specified\n\t\t\tinput: `{\"scope\": \"whywaita/whywaita2\", \"resource_type\": \"nano\", \"runner_user\": \"runner\", \"ghe_domain\": \"https://example.com\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tScope:          \"whywaita/whywaita2\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tf := func() {\n\t\t\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(test.input))\n\t\t\tif !test.err && err != nil {\n\t\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t\t}\n\t\t\tcontent, code := parseResponse(resp)\n\t\t\tif code != http.StatusCreated {\n\t\t\t\tt.Fatalf(\"must be response statuscode is 201, but got %d\", code)\n\t\t\t}\n\n\t\t\tvar gotContent web.UserTarget\n\t\t\tif err := json.Unmarshal(content, &gotContent); err != nil {\n\t\t\t\tt.Fatalf(\"failed to unmarshal resoponse content: %+v\", err)\n\t\t\t}\n\n\t\t\tgotContent.UUID = uuid.UUID{}\n\t\t\tgotContent.CreatedAt = time.Time{}\n\t\t\tgotContent.UpdatedAt = time.Time{}\n\n\t\t\tif diff := cmp.Diff(test.want, &gotContent); diff != \"\" {\n\t\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t}\n\n\t\tf()\n\t}\n}\n\nfunc Test_handleTargetCreate_alreadyRegistered(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\t_, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\tinput := `{\"scope\": \"octocat\", \"resource_type\": \"micro\", \"runner_user\": \"runner\", \"ghe_domain\": \"https://example.com\"}`\n\n\t// first create\n\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(input))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\t_, code := parseResponse(resp)\n\tif code != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d\", code)\n\t}\n\n\t// second create\n\tresp, err = http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(input))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\t_, code = parseResponse(resp)\n\tif code != http.StatusBadRequest {\n\t\tt.Fatalf(\"must be response statuscode is 400, but got %d\", code)\n\t}\n}\n\nfunc Test_handleTargetCreate_recreated(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\tinput := `{\"scope\": \"octocat\", \"resource_type\": \"micro\", \"runner_user\": \"runner\", \"ghe_domain\": \"https://example.com\"}`\n\n\t// first create\n\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(input))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\tcontent, code := parseResponse(resp)\n\tif code != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", code, string(content))\n\t}\n\tvar gotContent web.UserTarget\n\tif err := json.Unmarshal(content, &gotContent); err != nil {\n\t\tt.Fatalf(\"failed to unmarshal resoponse content: %+v\", err)\n\t}\n\n\tu := gotContent.UUID\n\n\t// first delete\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(\"%s/target/%s\", testURL, u.String()), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %+v\", err)\n\t}\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\t_, code = parseResponse(resp)\n\tif code != http.StatusNoContent {\n\t\tt.Fatalf(\"must be response statuscode is 204, but got %d: %+v\", code, string(content))\n\t}\n\n\t// second create\n\tresp, err = http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(input))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\tcontent, code = parseResponse(resp)\n\tif code != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", code, string(content))\n\t}\n\n\tgot, err := testDatastore.GetTarget(context.Background(), u)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get created target: %+v\", err)\n\t}\n\tif got.Status != datastore.TargetStatusActive {\n\t\tt.Fatalf(\"must be status is active when recreated\")\n\t}\n}\n\nfunc Test_handleTargetCreate_recreated_update(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\tinput := `{\"scope\": \"octocat\", \"resource_type\": \"micro\", \"runner_user\": \"runner\", \"ghe_domain\": \"https://example.com\"}`\n\n\t// first create\n\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(input))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\tcontent, code := parseResponse(resp)\n\tif code != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", code, string(content))\n\t}\n\tvar gotContent web.UserTarget\n\tif err := json.Unmarshal(content, &gotContent); err != nil {\n\t\tt.Fatalf(\"failed to unmarshal resoponse content: %+v\", err)\n\t}\n\n\tu := gotContent.UUID\n\n\t// first delete\n\tclient := &http.Client{}\n\treq, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(\"%s/target/%s\", testURL, u.String()), nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %+v\", err)\n\t}\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\t_, code = parseResponse(resp)\n\tif code != http.StatusNoContent {\n\t\tt.Fatalf(\"must be response statuscode is 204, but got %d: %+v\", code, string(content))\n\t}\n\n\t// second create\n\tsecondInput := `{\"scope\": \"octocat\", \"resource_type\": \"micro\", \"runner_user\": \"runner\", \"ghe_domain\": \"https://example.com\"}`\n\n\tresp, err = http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(secondInput))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\tcontent, code = parseResponse(resp)\n\tif code != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", code, string(content))\n\t}\n\n\tgot, err := testDatastore.GetTarget(context.Background(), u)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get created target: %+v\", err)\n\t}\n\tif got.Status != datastore.TargetStatusActive {\n\t\tt.Fatalf(\"must be status is active when recreated\")\n\t}\n}\n\nfunc Test_handleTargetList(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\t_, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\tfor _, rt := range []string{\"nano\", \"micro\"} {\n\t\ttarget := fmt.Sprintf(`{\"scope\": \"repo%s\", \"resource_type\": \"%s\", \"runner_user\": \"runner\"}`,\n\t\t\trt, rt)\n\t\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(target))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tif resp.StatusCode != http.StatusCreated {\n\t\t\tt.Fatalf(\"must be response statuscode is 201, but got %d\", resp.StatusCode)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tinput interface{}\n\t\twant  *[]web.UserTarget\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: nil,\n\t\t\twant: &[]web.UserTarget{\n\t\t\t\t{\n\t\t\t\t\tScope:          \"reponano\",\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tScope:          \"repomicro\",\n\t\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\t\tResourceType:   datastore.ResourceTypeMicro.String(),\n\t\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tresp, err := http.Get(testURL + \"/target\")\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontent, code := parseResponse(resp)\n\t\tif code != http.StatusOK {\n\t\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", code, string(content))\n\t\t}\n\n\t\tvar gotContents []web.UserTarget\n\t\tif err := json.Unmarshal(content, &gotContents); err != nil {\n\t\t\tt.Fatalf(\"failed to unmarshal resoponse content: %+v\", err)\n\t\t}\n\n\t\tfor i := range gotContents {\n\t\t\tgotContents[i].UUID = uuid.UUID{}\n\t\t\tgotContents[i].CreatedAt = time.Time{}\n\t\t\tgotContents[i].UpdatedAt = time.Time{}\n\t\t}\n\n\t\tif diff := cmp.Diff(test.want, &gotContents); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc Test_handleTargetRead(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\t_, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\ttarget := `{\"scope\": \"repo\", \"resource_type\": \"micro\", \"runner_user\": \"runner\"}`\n\n\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(target))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\tcontent, statusCode := parseResponse(resp)\n\tif statusCode != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", resp.StatusCode, string(content))\n\t}\n\tvar respTarget web.UserTarget\n\tif err := json.Unmarshal(content, &respTarget); err != nil {\n\t\tt.Fatalf(\"failed to unmarshal response JSON: %+v\", err)\n\t}\n\ttargetUUID := respTarget.UUID\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *web.UserTarget\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: targetUUID,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tUUID:           targetUUID,\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeMicro.String(),\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tresp, err := http.Get(fmt.Sprintf(\"%s/target/%s\", testURL, test.input))\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontent, code := parseResponse(resp)\n\t\tif code != http.StatusOK {\n\t\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", code, string(content))\n\t\t}\n\n\t\tvar got web.UserTarget\n\t\tif err := json.Unmarshal(content, &got); err != nil {\n\t\t\tt.Fatalf(\"failed to unmarshal resoponse content: %+v\", err)\n\t\t}\n\n\t\tgot.CreatedAt = time.Time{}\n\t\tgot.UpdatedAt = time.Time{}\n\n\t\tif diff := cmp.Diff(test.want, &got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\nfunc Test_handleTargetUpdate(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\t_, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\ttests := []struct {\n\t\tinput string\n\t\twant  *web.UserTarget\n\t\terr   bool\n\t}{\n\t\t{ // Update a few values\n\t\t\tinput: `{\"scope\": \"repo\", \"resource_type\": \"nano\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tUUID:           uuid.UUID{},\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\tProviderURL:    \"https://example.com/default-shoes\",\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t\t{ // Confirm that no error occurs even if ghe_domain is specified\n\t\t\tinput: `{\"scope\": \"repo\", \"resource_type\": \"nano\", \"ghe_domain\": \"https://example.com\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tUUID:           uuid.UUID{},\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\tProviderURL:    \"https://example.com/default-shoes\",\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t\t{ // Update all values\n\t\t\tinput: `{\"scope\": \"repo\", \"resource_type\": \"micro\", \"provider_url\": \"https://example.com/shoes-provider\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tUUID:           uuid.UUID{},\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeMicro.String(),\n\t\t\t\tProviderURL:    \"https://example.com/shoes-provider\",\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t\t{ // Update value only one, other value is not update\n\t\t\tinput: `{\"scope\": \"repo\", \"resource_type\": \"nano\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tUUID:           uuid.UUID{},\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\tProviderURL:    \"https://example.com/default-shoes\",\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t\t{ // Remove provider_url, Set blank\n\t\t\tinput: `{\"scope\": \"repo\", \"resource_type\": \"nano\" ,\"provider_url\": \"\"}`,\n\t\t\twant: &web.UserTarget{\n\t\t\t\tUUID:           uuid.UUID{},\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeNano.String(),\n\t\t\t\tProviderURL:    \"\",\n\t\t\t\tStatus:         datastore.TargetStatusActive,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttarget := `{\"scope\": \"repo\", \"resource_type\": \"micro\", \"runner_user\": \"runner\", \"provider_url\": \"https://example.com/default-shoes\"}`\n\t\trespCreate, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(target))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontentCreate, statusCode := parseResponse(respCreate)\n\t\tif statusCode != http.StatusCreated {\n\t\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", respCreate.StatusCode, string(contentCreate))\n\t\t}\n\t\tvar respTarget web.UserTarget\n\t\tif err := json.Unmarshal(contentCreate, &respTarget); err != nil {\n\t\t\tt.Fatalf(\"failed to unmarshal response JSON: %+v\", err)\n\t\t}\n\t\ttargetUUID := respTarget.UUID\n\n\t\tresp, err := http.Post(fmt.Sprintf(\"%s/target/%s\", testURL, targetUUID.String()), \"application/json\", bytes.NewBufferString(test.input))\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontent, code := parseResponse(resp)\n\t\tif code != http.StatusOK {\n\t\t\tt.Fatalf(\"must be response statuscode is 200, but got %d: %+v\", code, string(content))\n\t\t}\n\n\t\tvar got web.UserTarget\n\t\tif err := json.Unmarshal(content, &got); err != nil {\n\t\t\tt.Fatalf(\"failed to unmarshal resoponse content: %+v\", err)\n\t\t}\n\n\t\tgot.UUID = uuid.UUID{}\n\t\tgot.CreatedAt = time.Time{}\n\t\tgot.UpdatedAt = time.Time{}\n\n\t\tif diff := cmp.Diff(test.want, &got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\n\t\tteardown()\n\t}\n}\n\nfunc Test_handleTargetUpdate_Error(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\t_, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\ttests := []struct {\n\t\tinput    string\n\t\twantCode int\n\t\twant     string\n\t}{\n\t\t{ // Invalid: must set scope\n\t\t\tinput:    `{\"resource_type\": \"nano\", \"runner_user\": \"runner\"}`,\n\t\t\twantCode: http.StatusBadRequest,\n\t\t\twant:     `{\"error\":\"invalid input: can't updatable fields (Scope)\"}`,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttarget := `{\"scope\": \"repo\", \"resource_type\": \"micro\", \"runner_user\": \"runner\", \"provider_url\": \"https://example.com/default-shoes\"}`\n\t\trespCreate, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(target))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontentCreate, statusCode := parseResponse(respCreate)\n\t\tif statusCode != http.StatusCreated {\n\t\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", respCreate.StatusCode, string(contentCreate))\n\t\t}\n\t\tvar respTarget web.UserTarget\n\t\tif err := json.Unmarshal(contentCreate, &respTarget); err != nil {\n\t\t\tt.Fatalf(\"failed to unmarshal response JSON: %+v\", err)\n\t\t}\n\t\ttargetUUID := respTarget.UUID\n\n\t\tresp, err := http.Post(fmt.Sprintf(\"%s/target/%s\", testURL, targetUUID.String()), \"application/json\", bytes.NewBufferString(test.input))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontent, code := parseResponse(resp)\n\t\tgot := string(content)\n\t\tif code != test.wantCode {\n\t\t\tt.Fatalf(\"must be response statuscode is %d, but got %d: %+v\", test.wantCode, code, got)\n\t\t}\n\t\tif strings.EqualFold(test.want, got) {\n\t\t\tt.Fatalf(\"invalid error response: %+v\", string(content))\n\t\t}\n\n\t\tteardown()\n\t}\n}\n\nfunc Test_handleTargetDelete(t *testing.T) {\n\ttestURL := testutils.GetTestURL()\n\ttestDatastore, teardown := testutils.GetTestDatastore()\n\tdefer teardown()\n\n\tsetStubFunctions()\n\n\ttarget := `{\"scope\": \"repo\", \"resource_type\": \"micro\", \"runner_user\": \"runner\"}`\n\n\tresp, err := http.Post(testURL+\"/target\", \"application/json\", bytes.NewBufferString(target))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t}\n\tcontent, statusCode := parseResponse(resp)\n\tif statusCode != http.StatusCreated {\n\t\tt.Fatalf(\"must be response statuscode is 201, but got %d: %+v\", resp.StatusCode, string(content))\n\t}\n\tvar respTarget web.UserTarget\n\tif err := json.Unmarshal(content, &respTarget); err != nil {\n\t\tt.Fatalf(\"failed to unmarshal response JSON: %+v\", err)\n\t}\n\ttargetUUID := respTarget.UUID\n\n\ttests := []struct {\n\t\tinput uuid.UUID\n\t\twant  *datastore.Target\n\t\terr   bool\n\t}{\n\t\t{\n\t\t\tinput: targetUUID,\n\t\t\twant: &datastore.Target{\n\t\t\t\tUUID:           targetUUID,\n\t\t\t\tScope:          \"repo\",\n\t\t\t\tGitHubToken:    testGitHubAppToken,\n\t\t\t\tTokenExpiredAt: testTime,\n\t\t\t\tResourceType:   datastore.ResourceTypeMicro,\n\t\t\t\tStatus:         datastore.TargetStatusDeleted,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tclient := &http.Client{}\n\n\t\treq, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(\"%s/target/%s\", testURL, test.input), nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create request: %+v\", err)\n\t\t}\n\n\t\tresp, err := client.Do(req)\n\t\tif !test.err && err != nil {\n\t\t\tt.Fatalf(\"failed to POST request: %+v\", err)\n\t\t}\n\t\tcontent, code := parseResponse(resp)\n\t\tif code != http.StatusNoContent {\n\t\t\tt.Fatalf(\"must be response statuscode is 204, but got %d: %+v\", code, string(content))\n\t\t}\n\n\t\tgot, err := testDatastore.GetTarget(context.Background(), test.input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to get target from datastore: %+v\", err)\n\t\t}\n\n\t\tgot.CreatedAt = time.Time{}\n\t\tgot.UpdatedAt = time.Time{}\n\n\t\tif diff := cmp.Diff(test.want, got); diff != \"\" {\n\t\t\tt.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/web/webhook.go",
    "content": "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\"github.com/google/go-github/v80/github\"\n\tuuid \"github.com/satori/go.uuid\"\n\n\t\"github.com/whywaita/myshoes/pkg/config\"\n\t\"github.com/whywaita/myshoes/pkg/datastore\"\n\t\"github.com/whywaita/myshoes/pkg/gh\"\n\t\"github.com/whywaita/myshoes/pkg/logger\"\n\t\"github.com/whywaita/myshoes/pkg/metric\"\n)\n\n// HandleGitHubEvent handle GitHub webhook event\nfunc HandleGitHubEvent(w http.ResponseWriter, r *http.Request, ds datastore.Datastore) {\n\tctx := r.Context()\n\tstartTime := time.Now()\n\teventType := github.WebHookType(r)\n\n\tpayload, err := github.ValidatePayload(r, config.Config.GitHub.AppSecret)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to validate webhook payload: %+v\\n\", err)\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tmetric.WebhookReceivedTotal.WithLabelValues(eventType, \"invalid\", \"unknown\").Inc()\n\t\treturn\n\t}\n\twebhookEvent, err := github.ParseWebHook(eventType, payload)\n\tif err != nil {\n\t\tlogger.Logf(false, \"failed to parse webhook payload: %+v\\n\", err)\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tmetric.WebhookReceivedTotal.WithLabelValues(eventType, \"parse_error\", \"unknown\").Inc()\n\t\treturn\n\t}\n\n\t// Extract runs-on labels\n\trunsOn := \"unknown\"\n\tif eventType == \"workflow_job\" {\n\t\tlabels, err := gh.ExtractRunsOnLabels(payload)\n\t\tif err == nil && len(labels) > 0 {\n\t\t\trunsOn = strings.Join(labels, \",\")\n\t\t}\n\t}\n\n\tswitch event := webhookEvent.(type) {\n\tcase *github.PingEvent:\n\t\tif err := receivePingWebhook(ctx, event); err != nil {\n\t\t\tlogger.Logf(false, \"failed to process ping event: %+v\\n\", err)\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tmetric.WebhookReceivedTotal.WithLabelValues(\"ping\", \"error\", \"n/a\").Inc()\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t\tmetric.WebhookReceivedTotal.WithLabelValues(\"ping\", \"success\", \"n/a\").Inc()\n\t\tmetric.WebhookProcessingDuration.WithLabelValues(\"ping\", \"n/a\").Observe(time.Since(startTime).Seconds())\n\t\treturn\n\tcase *github.CheckRunEvent:\n\t\tif !config.Config.ModeWebhookType.Equal(\"check_run\") {\n\t\t\tlogger.Logf(false, \"receive CheckRunEvent, but set %s. So ignore\", config.Config.ModeWebhookType)\n\t\t\treturn\n\t\t}\n\n\t\tif err := receiveCheckRunWebhook(ctx, event, ds); err != nil {\n\t\t\tlogger.Logf(false, \"failed to process check_run event: %+v\\n\", err)\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tmetric.WebhookReceivedTotal.WithLabelValues(\"check_run\", \"error\", \"n/a\").Inc()\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t\tmetric.WebhookReceivedTotal.WithLabelValues(\"check_run\", \"success\", \"n/a\").Inc()\n\t\tmetric.WebhookProcessingDuration.WithLabelValues(\"check_run\", \"n/a\").Observe(time.Since(startTime).Seconds())\n\t\treturn\n\tcase *github.WorkflowJobEvent:\n\t\tif !config.Config.ModeWebhookType.Equal(\"workflow_job\") {\n\t\t\tlogger.Logf(false, \"receive WorkflowJobEvent, but set %s. So ignore\", config.Config.ModeWebhookType)\n\t\t\treturn\n\t\t}\n\n\t\tif err := receiveWorkflowJobWebhook(ctx, event, ds); err != nil {\n\t\t\tlogger.Logf(false, \"failed to process workflow_job event: %+v\\n\", err)\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tmetric.WebhookReceivedTotal.WithLabelValues(\"workflow_job\", \"error\", runsOn).Inc()\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t\tmetric.WebhookReceivedTotal.WithLabelValues(\"workflow_job\", \"success\", runsOn).Inc()\n\t\tmetric.WebhookProcessingDuration.WithLabelValues(\"workflow_job\", runsOn).Observe(time.Since(startTime).Seconds())\n\t\treturn\n\tdefault:\n\t\tlogger.Logf(false, \"receive not register event(%+v), return NotFound\", event)\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\tmetric.WebhookReceivedTotal.WithLabelValues(eventType, \"not_found\", \"unknown\").Inc()\n\t\treturn\n\t}\n}\n\nfunc receivePingWebhook(_ context.Context, event *github.PingEvent) error {\n\t// do nothing\n\treturn nil\n}\n\nfunc receiveCheckRunWebhook(ctx context.Context, event *github.CheckRunEvent, ds datastore.Datastore) error {\n\taction := event.GetAction()\n\tinstallationID := event.GetInstallation().GetID()\n\n\trepo := event.GetRepo()\n\trepoName := repo.GetFullName()\n\trepoURL := repo.GetHTMLURL()\n\n\tif action != \"created\" {\n\t\tlogger.Logf(true, \"check_action is not created, ignore (%s)\", action)\n\t\treturn nil\n\t}\n\n\tjb, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to json.Marshal: %w\", err)\n\t}\n\tif err := processCheckRun(ctx, ds, repoName, repoURL, installationID, jb); err != nil {\n\t\treturn err\n\t}\n\n\t// Record job enqueued metric\n\tmetric.WebhookJobsEnqueued.WithLabelValues(\"check_run\", repoName, \"n/a\").Inc()\n\n\treturn nil\n}\n\n// processCheckRun process webhook event\n// repoName is :owner/:repo\n// repoURL is https://github.com/:owenr/:repo (in github.com) or https://github.example.com/:owner/:repo (in GitHub Enterprise)\nfunc processCheckRun(ctx context.Context, ds datastore.Datastore, repoName, repoURL string, installationID int64, requestJSON []byte) error {\n\tif err := gh.CheckSignature(installationID); err != nil {\n\t\treturn fmt.Errorf(\"failed to create GitHub client: %w\", err)\n\t}\n\n\tu, err := url.Parse(repoURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse repository url from event: %w\", err)\n\t}\n\t//var domain string\n\tgheDomain := \"\"\n\tif u.Host != \"github.com\" {\n\t\tgheDomain = fmt.Sprintf(\"%s://%s\", u.Scheme, u.Host)\n\t}\n\n\tlogger.Logf(false, \"receive webhook repository: %s/%s\", gheDomain, repoName)\n\ttarget, err := datastore.SearchRepo(ctx, ds, repoName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to search registered target: %w\", err)\n\t}\n\n\tif !target.CanReceiveJob() {\n\t\t// do nothing if status is cannot receive\n\t\tlogger.Logf(false, \"%s/%s is %s now, do nothing\", gheDomain, repoName, target.Status)\n\t\treturn nil\n\t}\n\n\tjobID := uuid.NewV4()\n\tj := datastore.Job{\n\t\tUUID: jobID,\n\t\tGHEDomain: sql.NullString{\n\t\t\tString: gheDomain,\n\t\t\tValid:  gheDomain != \"\",\n\t\t},\n\t\tRepository:     repoName,\n\t\tCheckEventJSON: string(requestJSON),\n\t\tTargetID:       target.UUID,\n\t}\n\tif err := ds.EnqueueJob(ctx, j); err != nil {\n\t\treturn fmt.Errorf(\"failed to enqueue job: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc receiveWorkflowJobWebhook(ctx context.Context, event *github.WorkflowJobEvent, ds datastore.Datastore) error {\n\taction := event.GetAction()\n\tinstallationID := event.GetInstallation().GetID()\n\n\trepo := event.GetRepo()\n\trepoName := repo.GetFullName()\n\trepoURL := repo.GetHTMLURL()\n\n\tlabels := event.GetWorkflowJob().Labels\n\tif !gh.IsRequestedMyshoesLabel(labels) {\n\t\t// is not request myshoes, So will be ignored\n\t\tlogger.Logf(true, \"label \\\"myshoes\\\" is not found in labels, so ignore (labels: %s)\", labels)\n\t\treturn nil\n\t}\n\n\tif action != \"queued\" {\n\t\tlogger.Logf(true, \"workflow_job actions is not queued, ignore\")\n\t\treturn nil\n\t}\n\n\tjb, err := json.Marshal(event)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to json.Marshal: %w\", err)\n\t}\n\n\tif err := processCheckRun(ctx, ds, repoName, repoURL, installationID, jb); err != nil {\n\t\treturn err\n\t}\n\n\t// Record job enqueued metric for workflow_job\n\trunsOn := strings.Join(labels, \",\")\n\tmetric.WebhookJobsEnqueued.WithLabelValues(\"workflow_job\", repoName, runsOn).Inc()\n\n\treturn nil\n}\n"
  }
]