feat: use go sdk

This commit is contained in:
Louis Seubert 2024-11-01 23:41:40 +01:00
commit 666a8fc251
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
8 changed files with 220 additions and 162 deletions

21
Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM docker.io/golang:1.24-alpine AS compile
ENV CGO_ENABLED=0
WORKDIR /app
COPY go.mod go.sum /app/
RUN go mod download
COPY *.go /app/
RUN go build -o app .
FROM docker.io/alpine:3.22
WORKDIR /
RUN apk add git
COPY --from=compile /app/app /
ENTRYPOINT ["/app"]

View file

View file

@ -1,20 +1 @@
# checkout # checkout
This action checks-out your repository under $GITHUB_WORKSPACE, so your workflow can access it.
Only a single commit is fetched by default, for the ref/sha that triggered the workflow. The auth token
is persisted in the local git config. This enables your scripts to run authenticated git commands.
This action is only written in shell (bash).
## Usage
```yaml
uses: actions/checkout:1.0.0
with:
repository: ${{ github.server_url }}/${{ github.repository }}.git
token: ${{ github.token }}
path: .
ref: ${{ github.ref || github.sha }}
```

View file

@ -1,37 +1,23 @@
# SPDX-License-Identifier: EUPL-1.2 # SPDX-License-Identifier: EUPL-1.2
name: "Checkout" name: "Checkout"
description: "Checkout a Git repository at a particular version" description: "Checkout a Git repository at a particular version"
author: "Louis Seubert"
inputs: inputs:
repository: repository:
description: > description: >
Repository name with owner. For example, actions/checkout The path to the repository to checkout. Must be accessible with the
default: ${{ github.server_url }}/${{ github.repository }}.git pipeline token on publiclly
token: default: "${{ github.server_url }}/${{ github.repository }}.git"
description: >
Personal access token (PAT) used to fetch the repository. The PAT is configured
with the local git config, which enables your scripts to run authenticated git
commands.
default: ${{ github.token }}
path: path:
description: > description: >
Relative path under $GITHUB_WORKSPACE to place the repository Relative path under $GITHUB_WORKSPACE to place the repository
default: . default: "."
ref: ref:
description: > description: >
The branch, tag or SHA to checkout. When checking out the repository that The branch, tag or SHA to checkout. When checking out the repository that
triggered a workflow, this defaults to the reference or SHA for that triggered a workflow, this defaults to the reference or SHA for that
event. Otherwise, uses the default branch. event. Otherwise, uses the default branch.
default: ${{ github.ref || github.sha }} default: ${{ github.ref || github.sha }}
runs: runs:
using: composite using: 'docker'
steps: image: 'Dockerfile'
- name: Checkout
shell: bash
run: |
export PATH=${{ github.action_path }}/scripts:$PATH
checkout.sh
env:
ACTION_INPUT_TOKEN: ${{ inputs.token }}
ACTION_INPUT_REPOSITORY: ${{ inputs.repository }}
ACTION_INPUT_PATH: ${{ inputs.path }}
ACTION_INPUT_REF: ${{ inputs.ref }}

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module git.geekeey.de/actions/checkout
go 1.23.2
require git.geekeey.de/actions/sdk v1.1.0

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
git.geekeey.de/actions/sdk v1.1.0 h1:VrtMWo7hi3T4yQ4FL6P1XQkSeCA5Rl3UsSmBKITotLY=
git.geekeey.de/actions/sdk v1.1.0/go.mod h1:pBcHd6afsvseZF9hSc2A0b6+MldC/Ch6CSDaVlWMAmA=

184
main.go Normal file
View file

@ -0,0 +1,184 @@
package main
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"os/signal"
"strings"
"git.geekeey.de/actions/sdk"
)
func main() {
action := &CheckoutAction{Action: sdk.New()}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := action.Run(ctx); err != nil {
action.Errorf("%s", err)
os.Exit(1)
}
}
type CheckoutAction struct {
*sdk.Action
repository string
path string
reference string
}
func (action *CheckoutAction) setup() error {
var err error
action.repository = action.GetInput("repository")
if len(action.repository) == 0 {
return fmt.Errorf("input 'repository': is empty") // something like '<server>/<owner>/<repo>.git'
}
action.path = action.GetInput("path")
if len(action.path) == 0 {
return fmt.Errorf("input 'path': is empty") // something like '.'
}
action.reference = action.GetInput("ref")
if len(action.reference) == 0 {
return fmt.Errorf("input 'ref': is empty") // something like 'HEAD'
}
return err
}
func (action *CheckoutAction) Run(ctx context.Context) error {
if err := action.setup(); err != nil {
return err
}
env := action.Context()
server, err := url.Parse(env.ServerURL)
if err != nil {
return fmt.Errorf("invalid serevr url: %s", err)
}
shell := Shell{action: action.Action, args: [][]string{}}
shell.Add("git", "config", "--global", "--add", "protocol.version", "2")
shell.Add("git", "init", "-q", action.path)
shell.Add("git", "-C", action.path, "config", "--local", "gc.auto", "0")
if err := shell.Run(ctx, nil, nil); err != nil {
return err
}
action.Noticef("Configuring git repository credentials")
shell.Add("git", "-C", action.path, "config", "--local", "--add",
fmt.Sprintf("url.%s.insteadof", server.String()), fmt.Sprintf("git@%s", server.Host))
shell.Add("git", "-C", action.path, "config", "--local", "--add",
fmt.Sprintf("url.%s.insteadof", server.String()), fmt.Sprintf("ssh://git@%s", server.Host))
shell.Add("git", "-C", action.path, "config", "--local", "--add",
fmt.Sprintf("url.%s.insteadof", server.String()), fmt.Sprintf("git://%s", server.Host))
if err := shell.Run(ctx, nil, nil); err != nil {
return err
}
enc := base64.StdEncoding
header := fmt.Sprintf("Authorization: Basic %s", enc.EncodeToString(fmt.Appendf(nil, "x-access-token:%s", env.Token)))
shell.Add("git", "-C", action.path, "config", "--local", "--add", fmt.Sprintf("http.%s.extraheader", env.ServerURL), header)
if err := shell.Run(ctx, nil, nil); err != nil {
return err
}
origin := "origin"
shell.Add("git", "-C", action.path, "remote", "add", origin, action.repository)
if err := shell.Run(ctx, nil, nil); err != nil {
return err
}
shell.Add("git", "-C", action.path, "ls-remote", origin, action.reference)
var stdout strings.Builder = strings.Builder{}
if err := shell.Run(ctx, &stdout, nil); err != nil {
return err
}
lines := strings.Split(stdout.String(), "\t")
if len(lines) < 2 {
return fmt.Errorf("git ls-remore resolved nothing")
}
sha := strings.TrimSpace(lines[0])
ref := strings.TrimSpace(lines[1])
var name string
var ok bool
if name, ok = strings.CutPrefix(ref, "refs/heads/"); ok {
remote := fmt.Sprintf("refs/remotes/%s/%s", origin, name)
branch := name
action.Debugf("Checking out branch %s@%s", name, sha)
shell.Add("git", "-C", action.path, "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth=1",
origin, fmt.Sprintf("+%s:%s", sha, remote))
shell.Add("git", "-C", action.path, "checkout", "--force", "-B", branch, remote)
} else if name, ok = strings.CutPrefix(ref, "refs/pull/"); ok {
remote := fmt.Sprintf("refs/remotes/pull/%s", name)
action.Debugf("Checking out pull-request %s@%s", name, sha)
var branch string
if len(env.BaseRef) != 0 {
branch = env.BaseRef
} else if len(env.HeadRef) != 0 {
branch = env.HeadRef
} else {
return fmt.Errorf("pull request can not find base ref for branch")
}
shell.Add("git", "-C", action.path, "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth=1",
origin, fmt.Sprintf("+%s:%s", sha, remote))
shell.Add("git", "-C", action.path, "checkout", "--force", "-B", branch, remote)
} else if name, ok = strings.CutPrefix(ref, "refs/tags/"); ok {
remote := fmt.Sprintf("refs/tags/%s", name)
action.Debugf("Checking out tag %s@%s", name, sha)
shell.Add("git", "-C", action.path, "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth=1",
origin, fmt.Sprintf("+%s:%s", sha, remote))
shell.Add("git", "-C", action.path, "checkout", "--force", remote)
} else {
action.Debugf("Checking out detached head %s", ref)
shell.Add("git", "-C", action.path, "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth=1",
origin, ref)
shell.Add("git", "-C", action.path, "checkout", "--force", ref)
}
if err := shell.Run(ctx, nil, nil); err != nil {
return err
}
return nil
}
type Shell struct {
action *sdk.Action
args [][]string
}
func (sh *Shell) Add(args ...string) { sh.args = append(sh.args, args) }
func (sh *Shell) Run(ctx context.Context, stdout io.Writer, stderr io.Writer) error {
for _, args := range sh.args {
if sh.action != nil {
sh.action.Debugf("exec: %s", strings.Join(args, " "))
}
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
return err
}
}
sh.args = [][]string{}
return nil
}

View file

@ -1,121 +0,0 @@
#!/usr/bin/env bash
shopt -s extglob
ACTION_INPUT_TOKEN="${ACTION_INPUT_TOKEN:?}"
ACTION_INPUT_REPOSITORY="${ACTION_INPUT_REPOSITORY:?}"
ACTION_INPUT_PATH="${ACTION_INPUT_PATH:?}"
ACTION_INPUT_REF="${ACTION_INPUT_REF:?}"
error() {
echo "$*" 1>&2 ; exit 1;
}
# ensure we are in the right place
[[ -z "${GITHUB_WORKSPACE:-}" ]] || { cd "${GITHUB_WORKSPACE}" || error ; }
git config --global --add protocol.version 2
git init -q "${ACTION_INPUT_PATH}" && { cd "${ACTION_INPUT_PATH}" || error ; }
git config --local gc.auto 0
# the server url and uri are important for determining the default checkout url
GITHUB_SERVER_URL="${GITHUB_SERVER_URL:?}"
GITHUB_SERVER_URI="${GITHUB_SERVER_URL//http?(s):\/\//}"
# use https instead of everything else
git config --add url."${GITHUB_SERVER_URL}/".insteadOf "git@${GITHUB_SERVER_URI}:"
git config --add url."${GITHUB_SERVER_URL}/".insteadOf "ssh://git@${GITHUB_SERVER_URI}/"
git config --add url."${GITHUB_SERVER_URL}/".insteadOf "git://${GITHUB_SERVER_URI}/"
# prepare git config extra header for https auth
BASE64="base64 --wrap 0"
echo "" | ${BASE64} >/dev/null 2>&1 || BASE64="base64"
echo "" | ${BASE64} >/dev/null 2>&1 || BASE64="openssl base64 -A"
echo "" | ${BASE64} >/dev/null 2>&1
GIT_HTTP_EXTRAHEADER="AUTHORIZATION: basic $(echo -n "x-access-token:${ACTION_INPUT_TOKEN}" | ${BASE64})"
git config --local --add http."${GITHUB_SERVER_URL}".extraheader "${GIT_HTTP_EXTRAHEADER}"
# add the repository as remote
git remote add origin "${ACTION_INPUT_REPOSITORY}"
git_default_refspec() {
git ls-remote --symref origin HEAD | { read -r _ ref name ; echo "${ref}" ; }
}
git_resolve_refspec() {
# shellcheck disable=SC2068
git ls-remote origin $@ | { read -r sha ref _ ; echo "${sha} ${ref}" ; }
}
fetch() {
# shellcheck disable=SC2068
git fetch --no-tags --prune --no-recurse-submodules --depth=1 origin $@
}
checkout() {
# shellcheck disable=SC2068
git checkout --force $@
}
# the checkout is based on a commit hash
if [[ ${ACTION_INPUT_REF:-} =~ ^[0-9a-f]{5,40}$ ]]
then
fetch "${ACTION_INPUT_REF}" && checkout "${ACTION_INPUT_REF}"
exit
fi
# update selected ref when no input is given to remote default branch
: "${ACTION_INPUT_REF:=$( git_default_refspec )}"
# TODO: check if repo is workflow repo, if so, use commit sha from env
# like GITHUB_REF and GITHUB_SHA
read -r GIT_SHA GIT_REF _ <<< "$( git_resolve_refspec "${ACTION_INPUT_REF}" )"
: "${GIT_SHA:?}" "${GIT_REF:?}"
# we always use the refspec with the commit sha as source to prevent
# race conditions when running on a frequently used branch
checkout_head() {
local name="${GIT_REF#refs\/heads\/}"
local remote="refs/remotes/origin/${name}"
fetch "+${GIT_SHA}:${remote}"
checkout -B "${name}" "${remote}"
}
checkout_pull() {
local name="${GIT_REF#refs\/pull\/}"
local remote="refs/remotes/pull/${name}"
fetch "+${GIT_SHA}:${remote}"
# pull requests have a special treatment for the branch name. The name
# is determined by the branch the pull request is targeting.
local branch="${GITHUB_BASE_REF:-$( git_default_refspec )}"
checkout -B "${branch#refs\/heads\/}" "${remote}"
}
checkout_tag() {
local name="${GIT_REF#refs\/tags\/}"
local remote="refs/tags/${name}"
fetch "+${GIT_SHA}:${remote}"
checkout "${remote}"
}
case "${GIT_REF}" in
# heads and pull are a branch based checkout
refs/heads/*) checkout_head ;;
refs/pull/* ) checkout_pull ;;
# tags are a detached head checkout
refs/tags/* ) checkout_tag ;;
*) error "ref type '${GIT_REF}' is unknown" ;;
esac