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
|
secrets.env
|
||||||
apps/lix-builder/result
|
apps/lix-builder/result
|
||||||
|
apps/ci-os/dist/test
|
||||||
|
apps/ci-os/result
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
build-flakesaction:
|
build-ci-os:
|
||||||
desc: Builds and pushes the flakes action image
|
desc: Builds and pushes the flakes action image
|
||||||
dir: "{{ .ROOT_DIR }}/apps/lix-builder"
|
dir: "{{ .ROOT_DIR }}/apps/lix-builder"
|
||||||
cmds:
|
cmds:
|
||||||
- nix build .#packages.x86_64-linux.flakes-action && nerdctl load < result
|
- nix build .#packages.x86_64-linux.build-image && nerdctl load < result &&
|
||||||
&& nerdctl push code.252.no/tommy/flakes-action:latest
|
nerdctl push code.252.no/tommy/ci-os:latest
|
||||||
|
|
||||||
create-image:
|
create-image:
|
||||||
desc: Build local docker image (nixos-builder)
|
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
|
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
|
[Forgejo runners](https://code.forgejo.org/forgejo/runner). It packages essential tools and helper programs to streamline
|
||||||
|
@ -14,30 +14,31 @@ We try to reproduce much-used GitHub actions into one package to avoid calling a
|
||||||
|
|
||||||
### Docker Image Info
|
### 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.
|
- **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.
|
- **Essential Build Tools**: Includes utilities like `git`, `docker`, `bash`, `curl`, `jq`, and more.
|
||||||
|
|
||||||
### Nix Flake Info
|
### Nix Flake Info
|
||||||
|
|
||||||
- **Apps**: Accessible via `nix run` or `nix shell` commands.
|
- **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
|
### Helper Programs Provided
|
||||||
|
|
||||||
The flake provides the following applications:
|
The flake provides the following applications:
|
||||||
|
|
||||||
| Application | Description | External Ref |
|
| 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-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)
|
| `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-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).
|
[flake.nix](./flake.nix).
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,13 +46,13 @@ Other standard packages are provided from nixpkgs. For an up-to-date list have a
|
||||||
|
|
||||||
#### Building
|
#### 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`.
|
`nerdctl`, but you may replace this with `docker` or `podman`.
|
||||||
|
|
||||||
```bash
|
```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 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
|
#### In Runner
|
||||||
|
@ -82,7 +83,7 @@ To use the Docker image in your Forgejo runner add it to your Helm values (this
|
||||||
- "--instance"
|
- "--instance"
|
||||||
- $(FORGEJO_INSTANCE_URL)
|
- $(FORGEJO_INSTANCE_URL)
|
||||||
- "--labels"
|
- "--labels"
|
||||||
- "flakes-action:docker://code.252.no/tommy/flakes-action:latest,[...]"
|
- "ci-os:docker://code.252.no/tommy/ci-os:latest,[...]"
|
||||||
env:
|
env:
|
||||||
- name: RUNNER_TOKEN
|
- name: RUNNER_TOKEN
|
||||||
valueFrom:
|
valueFrom:
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
description = "docker base images";
|
description = "CI-OS - the Continuous Integration OS";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
@ -25,23 +25,34 @@
|
||||||
flux-local = flake-utils.lib.mkApp { drv = pkgs-local.flux-local; };
|
flux-local = flake-utils.lib.mkApp { drv = pkgs-local.flux-local; };
|
||||||
flux-diff = flake-utils.lib.mkApp { drv = pkgs-local.flux-diff; };
|
flux-diff = flake-utils.lib.mkApp { drv = pkgs-local.flux-diff; };
|
||||||
forgejo-comment = flake-utils.lib.mkApp { drv = pkgs-local.forgejo-comment; };
|
forgejo-comment = flake-utils.lib.mkApp { drv = pkgs-local.forgejo-comment; };
|
||||||
|
forgejo-release = flake-utils.lib.mkApp { drv = pkgs-local.forgejo-release; };
|
||||||
};
|
};
|
||||||
|
|
||||||
packages = {
|
packages = {
|
||||||
flakes-action = pkgs.dockerTools.buildImageWithNixDb {
|
build-image = pkgs.dockerTools.buildImageWithNixDb {
|
||||||
name = "code.252.no/tommy/flakes-action";
|
name = "code.252.no/tommy/ci-os";
|
||||||
tag = "latest";
|
tag = "latest";
|
||||||
copyToRoot = pkgs.buildEnv {
|
copyToRoot = pkgs.buildEnv {
|
||||||
name = "image-root";
|
name = "image-root";
|
||||||
pathsToLink = ["/bin" "/etc"];
|
pathsToLink = ["/bin" "/etc"];
|
||||||
ignoreCollisions = true;
|
ignoreCollisions = true;
|
||||||
paths = with pkgs; [
|
paths = with pkgs; [
|
||||||
gitSvPkg
|
# kubernetes
|
||||||
|
chart-testing
|
||||||
|
kubernetes-helm
|
||||||
|
kubernetes-polaris
|
||||||
|
pluto
|
||||||
pkgs-local.flux-local
|
pkgs-local.flux-local
|
||||||
|
pkgs-local.flux-diff
|
||||||
|
pkgs-local.forgejo-comment
|
||||||
|
pkgs-local.forgejo-release
|
||||||
|
|
||||||
findutils
|
# repository tooling
|
||||||
|
gitSvPkg
|
||||||
|
|
||||||
|
gnupg
|
||||||
coreutils-full
|
coreutils-full
|
||||||
|
findutils
|
||||||
python312Full
|
python312Full
|
||||||
docker
|
docker
|
||||||
bash
|
bash
|
|
@ -7,4 +7,5 @@ in
|
||||||
flux-local = fluxLocal;
|
flux-local = fluxLocal;
|
||||||
flux-diff = import ./flux-diff { inherit pkgs lib fluxLocal; };
|
flux-diff = import ./flux-diff { inherit pkgs lib fluxLocal; };
|
||||||
forgejo-comment = import ./forgejo-comment { inherit lib pkgs; };
|
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…
Add table
Reference in a new issue