chore: more restructuring of the repo. Most significant change is rename from flakes-action to ci-os
This commit is contained in:
parent
d535b2ba6e
commit
df7e141133
20 changed files with 1035 additions and 296 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
secrets.env
|
||||
apps/lix-builder/result
|
||||
apps/ci-os/dist/test
|
||||
apps/ci-os/result
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
version: "3"
|
||||
|
||||
tasks:
|
||||
build-flakesaction:
|
||||
build-ci-os:
|
||||
desc: Builds and pushes the flakes action image
|
||||
dir: "{{ .ROOT_DIR }}/apps/lix-builder"
|
||||
cmds:
|
||||
- nix build .#packages.x86_64-linux.flakes-action && nerdctl load < result
|
||||
&& nerdctl push code.252.no/tommy/flakes-action:latest
|
||||
- nix build .#packages.x86_64-linux.build-image && nerdctl load < result &&
|
||||
nerdctl push code.252.no/tommy/ci-os:latest
|
||||
|
||||
create-image:
|
||||
desc: Build local docker image (nixos-builder)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
## Flakes Action
|
||||
## CI-OS
|
||||
|
||||
This container is a little special since it provides a Nix flake designed to generate a docker image to use with
|
||||
[Forgejo runners](https://code.forgejo.org/forgejo/runner). It packages essential tools and helper programs to streamline
|
||||
|
@ -14,16 +14,16 @@ We try to reproduce much-used GitHub actions into one package to avoid calling a
|
|||
|
||||
### Docker Image Info
|
||||
|
||||
The latest container resulting from the nix build, is located in the registry at `code.252.no/tommy/flakes-action:latest` and includes:
|
||||
The latest container resulting from the nix build, is located in the registry at `code.252.no/tommy/ci-os:latest` and includes:
|
||||
|
||||
- **Nix Environment**: Pre-configured with Nix and essential configurations.
|
||||
- **Helper Programs**: Bundles `flux-local`, `flux-diff`, and `forgejo-comment`.
|
||||
- **Helper Programs**: Bundles `flux-local`, `flux-diff`, `forgejo-comment` and more.
|
||||
- **Essential Build Tools**: Includes utilities like `git`, `docker`, `bash`, `curl`, `jq`, and more.
|
||||
|
||||
### Nix Flake Info
|
||||
|
||||
- **Apps**: Accessible via `nix run` or `nix shell` commands.
|
||||
- **Packages**: Builds the `flakes-action` Docker image named `flakes-action` with necessary tools.
|
||||
- **Packages**: Builds the `ci-os` Docker image named `ci-os` with necessary tools.
|
||||
|
||||
|
||||
### Helper Programs Provided
|
||||
|
@ -31,13 +31,14 @@ The latest container resulting from the nix build, is located in the registry at
|
|||
The flake provides the following applications:
|
||||
|
||||
| Application | Description | External Ref |
|
||||
|-------------------|-----------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `git-sv` | Semantic versioning tool for git based on conventional commits. | [tommy/git-sv](https://code.252.no/tommy/git-sv)
|
||||
|-------------------|-----------------------------------------------------------------|------------------------------------------------------------------------------------------------------|
|
||||
| `git-sv` | Semantic versioning tool for git based on conventional commits. | [tommy/git-sv](https://code.252.no/tommy/git-sv) |
|
||||
| `flux-local` | Tool for performing local Flux operations and diffs. | [allenporter/flux-local](https://github.com/allenporter/flux-local) |
|
||||
| `flux-diff` | Utility to diff Flux resources locally. | [buroa/k8s-gitops](https://github.com/buroa/k8s-gitops/blob/master/.github/workflows/flux-diff.yaml)
|
||||
| `forgejo-comment` | Script to post comments on Forgejo merge requests. | -
|
||||
| `flux-diff` | Utility to diff Flux resources locally. | [buroa/k8s-gitops](https://github.com/buroa/k8s-gitops/blob/master/.github/workflows/flux-diff.yaml) |
|
||||
| `forgejo-comment` | Script to post comments on Forgejo merge requests. | - |
|
||||
| `forgejo-release` | Script to create releases in Forgejo. | - |
|
||||
|
||||
Other standard packages are provided from nixpkgs. For an up-to-date list have a look at `flakes-action` in
|
||||
Other standard packages are provided from nixpkgs. For an up-to-date list have a look at `ci-os` in
|
||||
[flake.nix](./flake.nix).
|
||||
|
||||
|
||||
|
@ -45,13 +46,13 @@ Other standard packages are provided from nixpkgs. For an up-to-date list have a
|
|||
|
||||
#### Building
|
||||
|
||||
We provide an example taskfile in `task docker:build-flakesaction` which is used for manual builds at `code.252.no`. The task uses
|
||||
We provide an example taskfile in `task docker:build-ci-os` which is used for manual builds at `code.252.no`. The task uses
|
||||
`nerdctl`, but you may replace this with `docker` or `podman`.
|
||||
|
||||
```bash
|
||||
nix build .#packages.x86_64-linux.flakes-action # build image
|
||||
nix build .#packages.x86_64-linux.ci-os # build image
|
||||
nerdctl load < result # loads nix build result (tar archive)
|
||||
nerdctl push code.252.no/tommy/flakes-action:latest # push to registry
|
||||
nerdctl push code.252.no/tommy/ci-os:latest # push to registry
|
||||
```
|
||||
|
||||
#### In Runner
|
||||
|
@ -82,7 +83,7 @@ To use the Docker image in your Forgejo runner add it to your Helm values (this
|
|||
- "--instance"
|
||||
- $(FORGEJO_INSTANCE_URL)
|
||||
- "--labels"
|
||||
- "flakes-action:docker://code.252.no/tommy/flakes-action:latest,[...]"
|
||||
- "ci-os:docker://code.252.no/tommy/ci-os:latest,[...]"
|
||||
env:
|
||||
- name: RUNNER_TOKEN
|
||||
valueFrom:
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
description = "docker base images";
|
||||
description = "CI-OS - the Continuous Integration OS";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
|
@ -25,23 +25,34 @@
|
|||
flux-local = flake-utils.lib.mkApp { drv = pkgs-local.flux-local; };
|
||||
flux-diff = flake-utils.lib.mkApp { drv = pkgs-local.flux-diff; };
|
||||
forgejo-comment = flake-utils.lib.mkApp { drv = pkgs-local.forgejo-comment; };
|
||||
forgejo-release = flake-utils.lib.mkApp { drv = pkgs-local.forgejo-release; };
|
||||
};
|
||||
|
||||
packages = {
|
||||
flakes-action = pkgs.dockerTools.buildImageWithNixDb {
|
||||
name = "code.252.no/tommy/flakes-action";
|
||||
build-image = pkgs.dockerTools.buildImageWithNixDb {
|
||||
name = "code.252.no/tommy/ci-os";
|
||||
tag = "latest";
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "image-root";
|
||||
pathsToLink = ["/bin" "/etc"];
|
||||
ignoreCollisions = true;
|
||||
paths = with pkgs; [
|
||||
gitSvPkg
|
||||
# kubernetes
|
||||
chart-testing
|
||||
kubernetes-helm
|
||||
kubernetes-polaris
|
||||
pluto
|
||||
pkgs-local.flux-local
|
||||
pkgs-local.flux-diff
|
||||
pkgs-local.forgejo-comment
|
||||
pkgs-local.forgejo-release
|
||||
|
||||
findutils
|
||||
# repository tooling
|
||||
gitSvPkg
|
||||
|
||||
gnupg
|
||||
coreutils-full
|
||||
findutils
|
||||
python312Full
|
||||
docker
|
||||
bash
|
|
@ -7,4 +7,5 @@ in
|
|||
flux-local = fluxLocal;
|
||||
flux-diff = import ./flux-diff { inherit pkgs lib fluxLocal; };
|
||||
forgejo-comment = import ./forgejo-comment { inherit lib pkgs; };
|
||||
forgejo-release = import ./forgejo-release { inherit lib pkgs; };
|
||||
}
|
13
apps/ci-os/packages/forgejo-comment/default.nix
Normal file
13
apps/ci-os/packages/forgejo-comment/default.nix
Normal file
|
@ -0,0 +1,13 @@
|
|||
{ lib, pkgs }:
|
||||
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "forgejo-comment";
|
||||
version = "1.0.0";
|
||||
|
||||
src = ./src;
|
||||
|
||||
# Vendor dependencies for reproducibility
|
||||
vendorHash = null;
|
||||
|
||||
subPackages = [ "." ];
|
||||
}
|
142
apps/ci-os/packages/forgejo-comment/src/main.go
Normal file
142
apps/ci-os/packages/forgejo-comment/src/main.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command-line flags
|
||||
var forgejoAPIURL string
|
||||
var token string
|
||||
var repoOwner string
|
||||
var repoName string
|
||||
var issueId int
|
||||
|
||||
flag.StringVar(&forgejoAPIURL, "forgejo-api-url", "https://code.252.no/api/v1", "Forgejo API URL")
|
||||
flag.StringVar(&token, "token", "", "Forgejo API token")
|
||||
flag.StringVar(&repoOwner, "repo-owner", "", "Repository owner")
|
||||
flag.StringVar(&repoName, "repo-name", "", "Repository name")
|
||||
flag.IntVar(&issueId, "issue-id", 0, "Issue id")
|
||||
flag.Parse()
|
||||
|
||||
// Get the diff file from the positional arguments
|
||||
args := flag.Args()
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Usage: forgejo-comment <diff-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
diffFile := args[0]
|
||||
|
||||
// Read environment variables if flags are not set
|
||||
if forgejoAPIURL == "" {
|
||||
forgejoAPIURL = os.Getenv("FORGEJO_API_URL")
|
||||
if forgejoAPIURL == "" {
|
||||
fmt.Println("Error: FORGEJO_API_URL is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token = os.Getenv("FORGEJO_TOKEN")
|
||||
if token == "" {
|
||||
fmt.Println("Error: FORGEJO_TOKEN is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if repoOwner == "" {
|
||||
repoOwner = os.Getenv("REPO_OWNER")
|
||||
if repoOwner == "" {
|
||||
fmt.Println("Error: REPO_OWNER is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if repoName == "" {
|
||||
repoName = os.Getenv("REPO_NAME")
|
||||
if repoName == "" {
|
||||
fmt.Println("Error: REPO_NAME is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if issueId == 0 {
|
||||
issueIdEnv := os.Getenv("ISSUE_ID")
|
||||
if issueIdEnv == "" {
|
||||
fmt.Println("Error: ISSUE_ID is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
var err error
|
||||
issueId, err = strconv.Atoi(issueIdEnv)
|
||||
if err != nil {
|
||||
fmt.Println("Error: Invalid ISSUE_ID")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Read the diff file
|
||||
diffContentBytes, err := os.ReadFile(diffFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading diff file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Prepare the comment content
|
||||
diffContent := string(diffContentBytes)
|
||||
commentBody := fmt.Sprintf("```diff\n%s\n```", diffContent)
|
||||
|
||||
// Prepare the request payload
|
||||
payload := map[string]string{
|
||||
"body": commentBody,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
fmt.Printf("Error marshaling payload: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Construct the API URL
|
||||
apiURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/comments", forgejoAPIURL, repoOwner, repoName, issueId)
|
||||
|
||||
// Create the HTTP request
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Send the request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Error sending request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check the response status code
|
||||
if resp.StatusCode != 201 {
|
||||
fmt.Printf("Failed to post comment. HTTP status: %d\n", resp.StatusCode)
|
||||
fmt.Printf("Response body: %s\n", string(respBody))
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Printf("Posted diff to Forgejo issue #%d\n", issueId)
|
||||
}
|
||||
}
|
0
apps/ci-os/packages/forgejo-release/README.md
Normal file
0
apps/ci-os/packages/forgejo-release/README.md
Normal file
13
apps/ci-os/packages/forgejo-release/default.nix
Normal file
13
apps/ci-os/packages/forgejo-release/default.nix
Normal file
|
@ -0,0 +1,13 @@
|
|||
{ lib, pkgs }:
|
||||
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "forgejo-release";
|
||||
version = "1.0.0";
|
||||
|
||||
src = ./src;
|
||||
|
||||
# Vendor dependencies for reproducibility
|
||||
vendorHash = null;
|
||||
|
||||
subPackages = [ "." ];
|
||||
}
|
3
apps/ci-os/packages/forgejo-release/src/go.mod
Normal file
3
apps/ci-os/packages/forgejo-release/src/go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module code.252.no/tommy/forgejo-release
|
||||
|
||||
go 1.22.6
|
826
apps/ci-os/packages/forgejo-release/src/main.go
Normal file
826
apps/ci-os/packages/forgejo-release/src/main.go
Normal file
|
@ -0,0 +1,826 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// API endpoint templates
|
||||
const (
|
||||
tagURLTemplate = "%s/api/v1/repos/%s/tags/%s"
|
||||
tagsURLTemplate = "%s/api/v1/repos/%s/tags"
|
||||
releasesURLTemplate = "%s/api/v1/repos/%s/releases"
|
||||
releaseByTagURLTemplate = "%s/api/v1/repos/%s/releases/tags/%s"
|
||||
releaseAssetsURLTemplate = "%s/api/v1/repos/%s/releases/%d/assets"
|
||||
releaseURLTemplate = "%s/api/v1/repos/%s/releases/%d"
|
||||
latestReleaseURLTemplate = "%s/api/v1/repos/%s/releases/latest"
|
||||
)
|
||||
|
||||
// Release represents a Forgejo release.
|
||||
type Release struct {
|
||||
ID int64 `json:"id"`
|
||||
Draft bool `json:"draft"`
|
||||
Tag string `json:"tag_name"`
|
||||
Assets []struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Parse command-line flags and environment variables.
|
||||
config, err := parseConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Configuration error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Execute the specified command.
|
||||
switch config.Command {
|
||||
case "create":
|
||||
err := createReleaseCommand(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Release creation failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "download":
|
||||
err := downloadRelease(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Download failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
default:
|
||||
fmt.Println("Unknown command. Use 'create' or 'download'.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Config holds the configuration parameters.
|
||||
type Config struct {
|
||||
ForgejoURL string
|
||||
Repo string
|
||||
Title string
|
||||
ReleaseDir string
|
||||
DownloadLatest bool
|
||||
TmpDir string
|
||||
Override bool
|
||||
Retry int
|
||||
Delay int
|
||||
GPGPrivateKey string
|
||||
GPGPassphrase string
|
||||
Tag string
|
||||
SHA string
|
||||
Token string
|
||||
Prerelease bool
|
||||
Command string
|
||||
Interactive bool
|
||||
}
|
||||
|
||||
// parseConfig parses command-line flags and environment variables.
|
||||
func parseConfig() (*Config, error) {
|
||||
var config Config
|
||||
|
||||
// Check if the program is run without arguments (interactive mode).
|
||||
if len(os.Args) == 1 {
|
||||
config.Interactive = true
|
||||
interactiveSetup(&config)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
flag.StringVar(&config.ForgejoURL, "forgejo-url", getEnv("FORGEJO", "https://code.252.no"), "Forgejo instance URL")
|
||||
flag.StringVar(&config.Repo, "repo", getEnv("REPO", ""), "Repository in the format owner/name (required)")
|
||||
flag.StringVar(&config.Title, "title", getEnv("TITLE", ""), "Release title (defaults to tag)")
|
||||
flag.StringVar(&config.ReleaseDir, "release-dir", getEnv("RELEASE_DIR", "dist/"), "Directory containing release assets")
|
||||
flag.BoolVar(&config.DownloadLatest, "download-latest", getEnvBool("DOWNLOAD_LATEST", false), "Download the latest release")
|
||||
flag.StringVar(&config.TmpDir, "tmp-dir", getEnv("TMP_DIR", ""), "Temporary directory")
|
||||
flag.BoolVar(&config.Override, "override", getEnvBool("OVERRIDE", false), "Override existing release and tag")
|
||||
flag.IntVar(&config.Retry, "retry", getEnvInt("RETRY", 1), "Number of retries when downloading")
|
||||
flag.IntVar(&config.Delay, "delay", getEnvInt("DELAY", 10), "Delay between retries in seconds")
|
||||
flag.StringVar(&config.GPGPrivateKey, "gpg-private-key", getEnv("GPG_PRIVATE_KEY", ""), "Path to GPG private key")
|
||||
flag.StringVar(&config.GPGPassphrase, "gpg-passphrase", getEnv("GPG_PASSPHRASE", ""), "Path to GPG passphrase file")
|
||||
flag.StringVar(&config.Tag, "tag", getEnv("TAG", ""), "Release tag (required for create)")
|
||||
flag.StringVar(&config.SHA, "sha", getEnv("SHA", ""), "Commit SHA for the tag (optional)")
|
||||
flag.StringVar(&config.Token, "token", getEnv("TOKEN", ""), "Forgejo API token (required)")
|
||||
flag.BoolVar(&config.Prerelease, "prerelease", getEnvBool("PRERELEASE", false), "Mark the release as a prerelease")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("Usage: %s <create|download> [options]\n", os.Args[0])
|
||||
fmt.Println("\nCommands:")
|
||||
fmt.Println(" create Create a new release with uploaded assets")
|
||||
fmt.Println(" download Download assets from an existing release")
|
||||
fmt.Println("\nOptions:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("\nExamples:")
|
||||
fmt.Printf(" %s create --repo=owner/repo --tag=v1.0.0 --token=your-token --release-dir=path/to/assets\n", os.Args[0])
|
||||
fmt.Printf(" %s download --repo=owner/repo --tag=v1.0.0 --token=your-token --release-dir=path/to/download\n", os.Args[0])
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Get the command (create or download).
|
||||
if len(flag.Args()) < 1 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
config.Command = flag.Args()[0]
|
||||
|
||||
// Validate required parameters.
|
||||
if config.Repo == "" {
|
||||
return nil, errors.New("repository (--repo) is required")
|
||||
}
|
||||
if config.Token == "" {
|
||||
return nil, errors.New("API token (--token) is required")
|
||||
}
|
||||
if config.Command == "create" && config.Tag == "" {
|
||||
return nil, errors.New("tag (--tag) is required for creating a release")
|
||||
}
|
||||
if config.Command == "download" && !config.DownloadLatest && config.Tag == "" {
|
||||
return nil, errors.New("tag (--tag) is required unless --download-latest is true")
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// interactiveSetup prompts the user for configuration in interactive mode.
|
||||
func interactiveSetup(config *Config) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Println("Interactive Mode: Please provide the following details.")
|
||||
|
||||
config.Command = promptChoice(reader, "Choose a command (create/download): ", []string{"create", "download"}, "create")
|
||||
config.ForgejoURL = prompt(reader, "Forgejo instance URL", getEnv("FORGEJO", "https://code.252.no"))
|
||||
config.Repo = prompt(reader, "Repository (owner/name)", "")
|
||||
config.Token = prompt(reader, "Forgejo API token", "")
|
||||
if config.Command == "create" {
|
||||
config.ReleaseDir = prompt(reader, "Directory containing release assets", getEnv("RELEASE_DIR", "dist/"))
|
||||
config.Tag = prompt(reader, "Release tag", "")
|
||||
config.Title = prompt(reader, "Release title", config.Tag)
|
||||
config.SHA = prompt(reader, "Commit SHA for the tag (leave blank if tag exists)", "")
|
||||
config.Prerelease = promptBool(reader, "Is this a prerelease?", false)
|
||||
config.Override = promptBool(reader, "Override existing release and tag if they exist?", false)
|
||||
config.GPGPrivateKey = prompt(reader, "Path to GPG private key (optional)", "")
|
||||
config.GPGPassphrase = prompt(reader, "Path to GPG passphrase file (optional)", "")
|
||||
} else if config.Command == "download" {
|
||||
config.ReleaseDir = prompt(reader, "Directory to download assets into", getEnv("RELEASE_DIR", "dist/"))
|
||||
config.DownloadLatest = promptBool(reader, "Download the latest release?", false)
|
||||
if !config.DownloadLatest {
|
||||
config.Tag = prompt(reader, "Release tag to download", "")
|
||||
}
|
||||
config.Retry = promptInt(reader, "Number of retries if release is not available", 1)
|
||||
config.Delay = promptInt(reader, "Delay between retries in seconds", 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for interactive prompts.
|
||||
func prompt(reader *bufio.Reader, label, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
fmt.Printf("%s [%s]: ", label, defaultValue)
|
||||
} else {
|
||||
fmt.Printf("%s: ", label)
|
||||
}
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func promptBool(reader *bufio.Reader, label string, defaultValue bool) bool {
|
||||
defaultStr := "no"
|
||||
if defaultValue {
|
||||
defaultStr = "yes"
|
||||
}
|
||||
for {
|
||||
fmt.Printf("%s [%s]: ", label, defaultStr)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
if input == "yes" || input == "y" {
|
||||
return true
|
||||
}
|
||||
if input == "no" || input == "n" {
|
||||
return false
|
||||
}
|
||||
fmt.Println("Please enter 'yes' or 'no'.")
|
||||
}
|
||||
}
|
||||
|
||||
func promptInt(reader *bufio.Reader, label string, defaultValue int) int {
|
||||
for {
|
||||
fmt.Printf("%s [%d]: ", label, defaultValue)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
value, err := strconv.Atoi(input)
|
||||
if err == nil {
|
||||
return value
|
||||
}
|
||||
fmt.Println("Please enter a valid integer.")
|
||||
}
|
||||
}
|
||||
|
||||
func promptChoice(reader *bufio.Reader, label string, choices []string, defaultValue string) string {
|
||||
choiceMap := make(map[string]bool)
|
||||
for _, choice := range choices {
|
||||
choiceMap[choice] = true
|
||||
}
|
||||
for {
|
||||
fmt.Printf("%s [%s]: ", label, defaultValue)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
if _, valid := choiceMap[input]; valid {
|
||||
return input
|
||||
}
|
||||
fmt.Printf("Invalid choice. Valid options are: %s\n", strings.Join(choices, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// createReleaseCommand handles the creation of a release.
|
||||
func createReleaseCommand(config *Config) error {
|
||||
// Create temporary directory if not provided.
|
||||
if config.TmpDir == "" {
|
||||
var err error
|
||||
config.TmpDir, err = os.MkdirTemp("", "forgejo-release")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(config.TmpDir)
|
||||
}
|
||||
|
||||
// Override existing release and tag if requested.
|
||||
if config.Override {
|
||||
if err := deleteRelease(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deleteTag(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the tag exists.
|
||||
if err := ensureTag(config); err != nil {
|
||||
return fmt.Errorf("failed to ensure tag: %w", err)
|
||||
}
|
||||
|
||||
// Sign release assets if GPG key is provided.
|
||||
if config.GPGPrivateKey != "" {
|
||||
if err := signReleaseAssets(config); err != nil {
|
||||
return fmt.Errorf("failed to sign assets: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create and publish the release.
|
||||
if err := createAndPublishRelease(config); err != nil {
|
||||
return fmt.Errorf("failed to create release: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Release '%s' created successfully.\n", config.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// uploadRelease handles the upload of a release.
|
||||
func uploadRelease(config *Config) error {
|
||||
// Create temporary directory if not provided.
|
||||
if config.TmpDir == "" {
|
||||
var err error
|
||||
config.TmpDir, err = os.MkdirTemp("", "forgejo-release")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(config.TmpDir)
|
||||
}
|
||||
|
||||
// Override existing release and tag if requested.
|
||||
if config.Override {
|
||||
if err := deleteRelease(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deleteTag(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the tag exists.
|
||||
if err := ensureTag(config); err != nil {
|
||||
return fmt.Errorf("failed to ensure tag: %w", err)
|
||||
}
|
||||
|
||||
// Sign release assets if GPG key is provided.
|
||||
if config.GPGPrivateKey != "" {
|
||||
if err := signReleaseAssets(config); err != nil {
|
||||
return fmt.Errorf("failed to sign assets: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create and publish the release.
|
||||
if err := createAndPublishRelease(config); err != nil {
|
||||
return fmt.Errorf("failed to create release: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadRelease handles the download of a release.
|
||||
func downloadRelease(config *Config) error {
|
||||
// Create temporary directory if not provided.
|
||||
if config.TmpDir == "" {
|
||||
var err error
|
||||
config.TmpDir, err = os.MkdirTemp("", "forgejo-release")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(config.TmpDir)
|
||||
}
|
||||
|
||||
// Retrieve the release information.
|
||||
release, err := getRelease(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Download the assets.
|
||||
if err := downloadAssets(config, release); err != nil {
|
||||
return fmt.Errorf("failed to download assets: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureTag checks if the tag exists and creates it if it doesn't.
|
||||
func ensureTag(config *Config) error {
|
||||
tagSHA, err := getTagSHA(config)
|
||||
if err != nil {
|
||||
if errors.Is(err, errTagNotFound) {
|
||||
// Tag does not exist; create it if SHA is provided.
|
||||
if config.SHA == "" {
|
||||
return errors.New("tag does not exist and SHA is not provided to create it")
|
||||
}
|
||||
if err := createTag(config); err != nil {
|
||||
return fmt.Errorf("failed to create tag: %w", err)
|
||||
}
|
||||
fmt.Printf("Tag '%s' created successfully.\n", config.Tag)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Tag exists; verify SHA if provided.
|
||||
if config.SHA != "" && config.SHA != tagSHA {
|
||||
return errors.New("tag SHA does not match the provided SHA")
|
||||
}
|
||||
// Use the tag's SHA if not provided.
|
||||
if config.SHA == "" {
|
||||
config.SHA = tagSHA
|
||||
}
|
||||
fmt.Printf("Tag '%s' already exists.\n", config.Tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTagSHA retrieves the SHA associated with the tag.
|
||||
func getTagSHA(config *Config) (string, error) {
|
||||
apiURL := fmt.Sprintf(tagURLTemplate, config.ForgejoURL, config.Repo, config.Tag)
|
||||
resp, err := apiRequest("GET", apiURL, nil, config.Token)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get tag: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var result struct {
|
||||
Commit struct {
|
||||
SHA string `json:"sha"`
|
||||
} `json:"commit"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("failed to decode tag response: %w", err)
|
||||
}
|
||||
return result.Commit.SHA, nil
|
||||
} else if resp.StatusCode == http.StatusNotFound {
|
||||
return "", errTagNotFound
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("failed to get tag: %s", string(bodyBytes))
|
||||
}
|
||||
|
||||
var errTagNotFound = errors.New("tag not found")
|
||||
|
||||
// createTag creates a new tag in the repository.
|
||||
func createTag(config *Config) error {
|
||||
payload := map[string]string{
|
||||
"tag_name": config.Tag,
|
||||
"target": config.SHA,
|
||||
}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
apiURL := fmt.Sprintf(tagsURLTemplate, config.ForgejoURL, config.Repo)
|
||||
resp, err := apiRequest("POST", apiURL, bytes.NewBuffer(payloadBytes), config.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create tag: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to create tag: %s", string(bodyBytes))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createAndPublishRelease creates a new release and uploads assets.
|
||||
func createAndPublishRelease(config *Config) error {
|
||||
// Create the release.
|
||||
releaseID, err := createRelease(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload assets.
|
||||
if err := uploadAssets(config, releaseID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Publish the release.
|
||||
if err := updateReleaseDraftState(config, releaseID, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createRelease creates a new release and returns its ID.
|
||||
func createRelease(config *Config) (int64, error) {
|
||||
isPrerelease := config.Prerelease || strings.Contains(strings.ToLower(config.Tag), "-rc")
|
||||
title := config.Title
|
||||
if title == "" {
|
||||
title = config.Tag
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"tag_name": config.Tag,
|
||||
"title": title,
|
||||
"prerelease": isPrerelease,
|
||||
"draft": true,
|
||||
"body": "",
|
||||
}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
|
||||
apiURL := fmt.Sprintf(releasesURLTemplate, config.ForgejoURL, config.Repo)
|
||||
resp, err := apiRequest("POST", apiURL, bytes.NewBuffer(payloadBytes), config.Token)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create release: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return 0, fmt.Errorf("failed to create release: %s", string(bodyBytes))
|
||||
}
|
||||
|
||||
var release Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return 0, fmt.Errorf("failed to decode release response: %w", err)
|
||||
}
|
||||
|
||||
return release.ID, nil
|
||||
}
|
||||
|
||||
// uploadAssets uploads the assets to the release.
|
||||
func uploadAssets(config *Config, releaseID int64) error {
|
||||
entries, err := os.ReadDir(config.ReleaseDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read release directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
filePath := filepath.Join(config.ReleaseDir, entry.Name())
|
||||
if err := uploadAsset(config, releaseID, filePath); err != nil {
|
||||
return fmt.Errorf("failed to upload asset %s: %w", entry.Name(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// uploadAsset uploads a single asset to the release.
|
||||
func uploadAsset(config *Config, releaseID int64, filePath string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open asset file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file info: %w", err)
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", fileInfo.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create form file: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return fmt.Errorf("failed to copy file content: %w", err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close multipart writer: %w", err)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf(releaseAssetsURLTemplate, config.ForgejoURL, config.Repo, releaseID)
|
||||
req, err := http.NewRequest("POST", apiURL, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create upload request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", config.Token))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload asset: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to upload asset: %s", string(bodyBytes))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateReleaseDraftState updates the draft state of the release.
|
||||
func updateReleaseDraftState(config *Config, releaseID int64, state bool) error {
|
||||
payload := map[string]bool{
|
||||
"draft": state,
|
||||
}
|
||||
payloadBytes, _ := json.Marshal(payload)
|
||||
apiURL := fmt.Sprintf(releaseURLTemplate, config.ForgejoURL, config.Repo, releaseID)
|
||||
resp, err := apiRequest("PATCH", apiURL, bytes.NewBuffer(payloadBytes), config.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update release draft state: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to update release draft state: %s", string(bodyBytes))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// signReleaseAssets signs the release assets using GPG.
|
||||
func signReleaseAssets(config *Config) error {
|
||||
gpgHome, err := os.MkdirTemp("", "gpg")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GPG home directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(gpgHome)
|
||||
|
||||
// Import GPG private key.
|
||||
gpgArgs := []string{"--batch", "--yes", "--import", config.GPGPrivateKey}
|
||||
if config.GPGPassphrase != "" {
|
||||
gpgArgs = append([]string{"--passphrase-file", config.GPGPassphrase}, gpgArgs...)
|
||||
}
|
||||
cmd := exec.Command("gpg", gpgArgs...)
|
||||
cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to import GPG key: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// Sign each asset.
|
||||
entries, err := os.ReadDir(config.ReleaseDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read release directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || strings.HasSuffix(entry.Name(), ".sha256") {
|
||||
continue
|
||||
}
|
||||
filePath := filepath.Join(config.ReleaseDir, entry.Name())
|
||||
sigPath := filePath + ".asc"
|
||||
|
||||
cmdArgs := []string{"--armor", "--detach-sign", "--output", sigPath, filePath}
|
||||
if config.GPGPassphrase != "" {
|
||||
cmdArgs = append([]string{"--batch", "--yes", "--passphrase-file", config.GPGPassphrase}, cmdArgs...)
|
||||
}
|
||||
cmd = exec.Command("gpg", cmdArgs...)
|
||||
cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to sign asset %s: %w, output: %s", entry.Name(), err, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteRelease deletes an existing release by tag.
|
||||
func deleteRelease(config *Config) error {
|
||||
apiURL := fmt.Sprintf(releaseByTagURLTemplate, config.ForgejoURL, config.Repo, config.Tag)
|
||||
resp, err := apiRequest("DELETE", apiURL, nil, config.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete release: %w", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteTag deletes an existing tag.
|
||||
func deleteTag(config *Config) error {
|
||||
apiURL := fmt.Sprintf("%s/api/v1/repos/%s/tags/%s", config.ForgejoURL, config.Repo, config.Tag)
|
||||
resp, err := apiRequest("DELETE", apiURL, nil, config.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete tag: %w", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRelease retrieves the release information.
|
||||
func getRelease(config *Config) (*Release, error) {
|
||||
var release *Release
|
||||
var err error
|
||||
|
||||
for i := 0; i < config.Retry; i++ {
|
||||
if config.DownloadLatest {
|
||||
release, err = getLatestRelease(config)
|
||||
} else {
|
||||
release, err = getReleaseByTag(config)
|
||||
}
|
||||
|
||||
if err == nil && !release.Draft {
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Printf("Release not ready yet, retrying in %d seconds...\n", config.Delay)
|
||||
time.Sleep(time.Duration(config.Delay) * time.Second)
|
||||
}
|
||||
|
||||
if release == nil || release.Draft {
|
||||
return nil, errors.New("release not available")
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
// getLatestRelease retrieves the latest release.
|
||||
func getLatestRelease(config *Config) (*Release, error) {
|
||||
apiURL := fmt.Sprintf(latestReleaseURLTemplate, config.ForgejoURL, config.Repo)
|
||||
resp, err := apiRequest("GET", apiURL, nil, config.Token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get latest release: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("failed to get latest release: %s", string(bodyBytes))
|
||||
}
|
||||
|
||||
var release Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode latest release: %w", err)
|
||||
}
|
||||
return &release, nil
|
||||
}
|
||||
|
||||
// getReleaseByTag retrieves a release by its tag.
|
||||
func getReleaseByTag(config *Config) (*Release, error) {
|
||||
apiURL := fmt.Sprintf("%s/api/v1/repos/%s/releases/tags/%s", config.ForgejoURL, config.Repo, config.Tag)
|
||||
resp, err := apiRequest("GET", apiURL, nil, config.Token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get release by tag: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("failed to get release by tag: %s", string(bodyBytes))
|
||||
}
|
||||
|
||||
var release Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode release by tag: %w", err)
|
||||
}
|
||||
return &release, nil
|
||||
}
|
||||
|
||||
func downloadAssets(config *Config, release *Release) error {
|
||||
if err := os.MkdirAll(config.ReleaseDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create release directory: %w", err)
|
||||
}
|
||||
|
||||
for _, asset := range release.Assets {
|
||||
assetPath := filepath.Join(config.ReleaseDir, asset.Name)
|
||||
if err := downloadAsset(config, asset.BrowserDownloadURL, assetPath); err != nil {
|
||||
return fmt.Errorf("failed to download asset %s: %w", asset.Name, err)
|
||||
}
|
||||
fmt.Printf("Downloaded asset: %s\n", asset.Name)
|
||||
}
|
||||
|
||||
fmt.Println("All assets downloaded successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadAsset downloads a single asset.
|
||||
func downloadAsset(config *Config, url, filepath string) error {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create download request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", config.Token))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download asset: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("failed to download asset: %s", string(bodyBytes))
|
||||
}
|
||||
|
||||
outFile, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err := io.Copy(outFile, resp.Body); err != nil {
|
||||
return fmt.Errorf("failed to write asset to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// apiRequest sends an HTTP request to the Forgejo API.
|
||||
func apiRequest(method, url string, body io.Reader, token string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create API request: %w", err)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API request failed: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Helper functions to get environment variables with defaults.
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
if valueStr, exists := os.LookupEnv(key); exists {
|
||||
if value, err := strconv.Atoi(valueStr); err == nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvBool(key string, defaultValue bool) bool {
|
||||
if valueStr, exists := os.LookupEnv(key); exists {
|
||||
if value, err := strconv.ParseBool(valueStr); err == nil {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: builds
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
pod-security.kubernetes.io/enforce-version: latest
|
||||
pod-security.kubernetes.io/warn: privileged
|
||||
pod-security.kubernetes.io/warn-version: latest
|
||||
pod-security.kubernetes.io/audit: privileged
|
||||
pod-security.kubernetes.io/audit-version: latest
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: kaniko
|
||||
namespace: builds
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
fsGroup: 1001
|
||||
seccompProfile:
|
||||
type: Unconfined
|
||||
containers:
|
||||
- name: kaniko
|
||||
image: code.252.no/tommy/kaniko:v24.10.01@sha256:d51c3b5c468bb070108d9e27884072f8527f20c9e41e2133621c56f62f89afc0
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 2Gi
|
||||
command: ["/opt/kaniko/kaniko"]
|
||||
args:
|
||||
- --dockerfile=Dockerfile
|
||||
#- --reproducible
|
||||
- --context=/kaniko
|
||||
- --custom-platform=linux/amd64
|
||||
- --destination=code.252.no/tommy/lix-builder:v24.10.01
|
||||
#- --dockerfile=Dockerfile
|
||||
#- --reproducible
|
||||
#- --kaniko-dir=/workspace/kaniko
|
||||
#- --context=/workspace
|
||||
#- --custom-platform=linux/amd64
|
||||
#- --destination=code.252.no/tommy/lix-builder:v24.10.01
|
||||
#- --cache=true
|
||||
#- --compressed-caching=false
|
||||
#- --use-new-run
|
||||
#- --cleanup
|
||||
volumeMounts:
|
||||
# - name: workspace-dir
|
||||
# mountPath: /workspace
|
||||
- name: docker-config
|
||||
mountPath: /opt/kaniko/.docker/config.json
|
||||
subPath: config.json
|
||||
- name: dockerfile
|
||||
mountPath: /kaniko/Dockerfile
|
||||
subPath: Dockerfile
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
privileged: false
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- FOWNER
|
||||
- DAC_OVERRIDE
|
||||
- SYS_ADMIN
|
||||
restartPolicy: Never
|
||||
volumes:
|
||||
# - name: workspace-dir
|
||||
# emptyDir: {}
|
||||
- name: docker-config
|
||||
secret:
|
||||
secretName: tommy-pushsecret-rw
|
||||
items:
|
||||
- key: .dockerconfigjson
|
||||
path: config.json
|
||||
- name: dockerfile
|
||||
configMap:
|
||||
name: dockerfile
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dockerfile
|
||||
namespace: builds
|
||||
data:
|
||||
Dockerfile: |
|
||||
FROM ghcr.io/lix-project/lix:2.91
|
||||
|
||||
WORKDIR /tmp/working-dir
|
||||
|
||||
RUN nix-env -iA nixpkgs.go nixpkgs.vim nixpkgs.sops nixpkgs.nix-direnv \
|
||||
nixpkgs.attic-client nixpkgs.nh nixpkgs.deploy-rs nixpkgs.statix \
|
||||
nixpkgs.deadnix nixpkgs.alejandra nixpkgs.home-manager \
|
||||
nixpkgs.ssh-to-age nixpkgs.gnupg nixpkgs.age nixpkgs.linux \
|
||||
nixpkgs.go-task nixpkgs.curl nixpkgs.fish nixpkgs.nixos-anywhere
|
||||
|
||||
ENTRYPOINT ["fish"]
|
|
@ -1,9 +0,0 @@
|
|||
app: lix-builder
|
||||
version: v24.10.01
|
||||
channels:
|
||||
- name: stable
|
||||
platforms: ["linux/amd64"]
|
||||
stable: false
|
||||
tests:
|
||||
enabled: true
|
||||
type: cli
|
|
@ -1,20 +0,0 @@
|
|||
{ lib, pkgs }:
|
||||
|
||||
pkgs.buildGoModule rec {
|
||||
pname = "forgejo-comment";
|
||||
version = "1.0.0";
|
||||
|
||||
src = ./src;
|
||||
|
||||
# Vendor dependencies for reproducibility
|
||||
vendorHash = null;
|
||||
|
||||
subPackages = [ "." ];
|
||||
|
||||
meta = {
|
||||
homepage = "https://code.252.no/tommy/containers";
|
||||
description = "Comment on Forgejo issues via the Forgejo API";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ "tommy-skaug" ];
|
||||
};
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command-line flags
|
||||
var forgejoAPIURL string
|
||||
var token string
|
||||
var repoOwner string
|
||||
var repoName string
|
||||
var issueIndex int
|
||||
|
||||
flag.StringVar(&forgejoAPIURL, "forgejo-api-url", "", "Forgejo API URL")
|
||||
flag.StringVar(&token, "token", "", "Forgejo API token")
|
||||
flag.StringVar(&repoOwner, "repo-owner", "", "Repository owner")
|
||||
flag.StringVar(&repoName, "repo-name", "", "Repository name")
|
||||
flag.IntVar(&issueIndex, "issue-index", 0, "Issue index")
|
||||
flag.Parse()
|
||||
|
||||
// Get the diff file from the positional arguments
|
||||
args := flag.Args()
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Usage: forgejo-comment <diff-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
diffFile := args[0]
|
||||
|
||||
// Read environment variables if flags are not set
|
||||
if forgejoAPIURL == "" {
|
||||
forgejoAPIURL = os.Getenv("FORGEJO_API_URL")
|
||||
if forgejoAPIURL == "" {
|
||||
fmt.Println("Error: FORGEJO_API_URL is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
token = os.Getenv("FORGEJO_TOKEN")
|
||||
if token == "" {
|
||||
fmt.Println("Error: FORGEJO_TOKEN is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if repoOwner == "" {
|
||||
repoOwner = os.Getenv("REPO_OWNER")
|
||||
if repoOwner == "" {
|
||||
fmt.Println("Error: REPO_OWNER is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if repoName == "" {
|
||||
repoName = os.Getenv("REPO_NAME")
|
||||
if repoName == "" {
|
||||
fmt.Println("Error: REPO_NAME is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if issueIndex == 0 {
|
||||
issueIndexEnv := os.Getenv("ISSUE_INDEX")
|
||||
if issueIndexEnv == "" {
|
||||
fmt.Println("Error: ISSUE_INDEX is not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
var err error
|
||||
issueIndex, err = strconv.Atoi(issueIndexEnv)
|
||||
if err != nil {
|
||||
fmt.Println("Error: Invalid ISSUE_INDEX")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Read the diff file
|
||||
diffContentBytes, err := ioutil.ReadFile(diffFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading diff file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Prepare the comment content
|
||||
diffContent := string(diffContentBytes)
|
||||
commentBody := fmt.Sprintf("```diff\n%s\n```", diffContent)
|
||||
|
||||
// Prepare the request payload
|
||||
payload := map[string]string{
|
||||
"body": commentBody,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
fmt.Printf("Error marshaling payload: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Construct the API URL
|
||||
apiURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/comments", forgejoAPIURL, repoOwner, repoName, issueIndex)
|
||||
|
||||
// Create the HTTP request
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Send the request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Error sending request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Error reading response: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check the response status code
|
||||
if resp.StatusCode != 201 {
|
||||
fmt.Printf("Failed to post comment. HTTP status: %d\n", resp.StatusCode)
|
||||
fmt.Printf("Response body: %s\n", string(respBody))
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Printf("Posted diff to Forgejo issue #%d\n", issueIndex)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue