Implement migration system, add first migration
Thank you for the help Chris! https://github.com/whereswaldon
This commit is contained in:
		
							parent
							
								
									fc27ee8438
								
							
						
					
					
						commit
						0675278fe2
					
				| 
						 | 
					@ -63,18 +63,18 @@ func main() {
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Println("Verifying database schema")
 | 
						fmt.Println("Checking whether database needs initialising")
 | 
				
			||||||
	err = db.VerifySchema(dbConn)
 | 
						err = db.InitialiseDatabase(dbConn)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Println("Error verifying database schema:", err)
 | 
							fmt.Println("Error initialising database:", err)
 | 
				
			||||||
		fmt.Println("Attempting to load schema")
 | 
					 | 
				
			||||||
		err = db.LoadSchema(dbConn)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			fmt.Println("Error loading schema:", err)
 | 
					 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						fmt.Println("Checking whether there are pending migrations")
 | 
				
			||||||
 | 
						err = db.Migrate(dbConn)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Println("Error migrating database schema:", err)
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmt.Println("Database schema verified")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(*flagAddUser) > 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
 | 
						if len(*flagAddUser) > 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
 | 
				
			||||||
		createUser(dbConn, *flagAddUser)
 | 
							createUser(dbConn, *flagAddUser)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								db/db.go
								
								
								
								
							
							
						
						
									
										38
									
								
								db/db.go
								
								
								
								
							| 
						 | 
					@ -6,46 +6,54 @@ package db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
	"embed"
 | 
						_ "embed"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_ "modernc.org/sqlite"
 | 
						_ "modernc.org/sqlite"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Embed the schema into the binary
 | 
					//go:embed sql/schema.sql
 | 
				
			||||||
//
 | 
					var schema string
 | 
				
			||||||
//go:embed sql
 | 
					 | 
				
			||||||
var embeddedSQL embed.FS
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Open opens a connection to the SQLite database
 | 
					// Open opens a connection to the SQLite database
 | 
				
			||||||
func Open(dbPath string) (*sql.DB, error) {
 | 
					func Open(dbPath string) (*sql.DB, error) {
 | 
				
			||||||
	return sql.Open("sqlite", dbPath)
 | 
						return sql.Open("sqlite", dbPath)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func VerifySchema(dbConn *sql.DB) error {
 | 
					// VerifySchema checks whether the schema has been initalised and initialises it
 | 
				
			||||||
 | 
					// if not
 | 
				
			||||||
 | 
					func InitialiseDatabase(dbConn *sql.DB) error {
 | 
				
			||||||
 | 
						var name string
 | 
				
			||||||
 | 
						err := dbConn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_migrations'").Scan(&name)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tables := []string{
 | 
						tables := []string{
 | 
				
			||||||
		"users",
 | 
							"users",
 | 
				
			||||||
		"sessions",
 | 
							"sessions",
 | 
				
			||||||
		"projects",
 | 
							"projects",
 | 
				
			||||||
 | 
							"releases",
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, table := range tables {
 | 
						for _, table := range tables {
 | 
				
			||||||
		name := ""
 | 
							name := ""
 | 
				
			||||||
		err := dbConn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
 | 
							err := dbConn.QueryRow(
 | 
				
			||||||
 | 
								"SELECT name FROM sqlite_master WHERE type='table' AND name=@table",
 | 
				
			||||||
 | 
								sql.Named("table", table),
 | 
				
			||||||
 | 
							).Scan(&name)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if err = loadSchema(dbConn); err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadSchema loads the schema into the database
 | 
					// loadSchema loads the initial schema into the database
 | 
				
			||||||
func LoadSchema(dbConn *sql.DB) error {
 | 
					func loadSchema(dbConn *sql.DB) error {
 | 
				
			||||||
	schema, err := embeddedSQL.ReadFile("sql/schema.sql")
 | 
						if _, err := dbConn.Exec(schema); err != nil {
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
	_, err = dbConn.Exec(string(schema))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,133 @@
 | 
				
			||||||
 | 
					// SPDX-FileCopyrightText: Chris Waldon <christopher.waldon.dev@gmail.com>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: Apache-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						_ "embed"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type migration struct {
 | 
				
			||||||
 | 
						upQuery   string
 | 
				
			||||||
 | 
						downQuery string
 | 
				
			||||||
 | 
						postHook  func(*sql.Tx) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						//go:embed sql/1_add_project_ids.up.sql
 | 
				
			||||||
 | 
						migration1Up string
 | 
				
			||||||
 | 
						//go:embed sql/1_add_project_ids.down.sql
 | 
				
			||||||
 | 
						migration1Down string
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var migrations = [...]migration{
 | 
				
			||||||
 | 
						0: {
 | 
				
			||||||
 | 
							upQuery: `CREATE TABLE schema_migrations (version uint64, dirty bool);
 | 
				
			||||||
 | 
							INSERT INTO schema_migrations (version, dirty) VALUES (0, 0);`,
 | 
				
			||||||
 | 
							downQuery: `DROP TABLE schema_migrations;`,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						1: {
 | 
				
			||||||
 | 
							upQuery:   migration1Up,
 | 
				
			||||||
 | 
							downQuery: migration1Down,
 | 
				
			||||||
 | 
							postHook:  generateAndInsertProjectIDs,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Migrate runs all pending migrations
 | 
				
			||||||
 | 
					func Migrate(db *sql.DB) error {
 | 
				
			||||||
 | 
						version := getSchemaVersion(db)
 | 
				
			||||||
 | 
						for nextMigration := version + 1; nextMigration < len(migrations); nextMigration++ {
 | 
				
			||||||
 | 
							if err := runMigration(db, nextMigration); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("migrations failed: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if version := getSchemaVersion(db); version != nextMigration {
 | 
				
			||||||
 | 
								return fmt.Errorf("migration did not update version (expected %d, got %d)", nextMigration, version)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// runMigration runs a single migration inside a transaction, updates the schema
 | 
				
			||||||
 | 
					// version and commits the transaction if successful, and rolls back the
 | 
				
			||||||
 | 
					// transaction if unsuccessful.
 | 
				
			||||||
 | 
					func runMigration(db *sql.DB, migrationIdx int) (err error) {
 | 
				
			||||||
 | 
						current := migrations[migrationIdx]
 | 
				
			||||||
 | 
						tx, err := db.BeginTx(context.Background(), &sql.TxOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed opening transaction for migration %d: %w", migrationIdx, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								err = tx.Commit()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if rbErr := tx.Rollback(); rbErr != nil {
 | 
				
			||||||
 | 
									err = fmt.Errorf("failed rolling back: %w due to: %w", rbErr, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						if len(current.upQuery) > 0 {
 | 
				
			||||||
 | 
							if _, err := tx.Exec(current.upQuery); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed running migration %d: %w", migrationIdx, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if current.postHook != nil {
 | 
				
			||||||
 | 
							if err := current.postHook(tx); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed running posthook for migration %d: %w", migrationIdx, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return updateSchemaVersion(tx, migrationIdx)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// undoMigration rolls the single most recent migration back inside a
 | 
				
			||||||
 | 
					// transaction, updates the schema version and commits the transaction if
 | 
				
			||||||
 | 
					// successful, and rolls back the transaction if unsuccessful.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//lint:ignore U1000 Will be used when #34 is implemented (https://todo.sr.ht/~amolith/willow/34)
 | 
				
			||||||
 | 
					func undoMigration(db *sql.DB, migrationIdx int) (err error) {
 | 
				
			||||||
 | 
						current := migrations[migrationIdx]
 | 
				
			||||||
 | 
						tx, err := db.BeginTx(context.Background(), &sql.TxOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed opening undo transaction for migration %d: %w", migrationIdx, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								err = tx.Commit()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if rbErr := tx.Rollback(); rbErr != nil {
 | 
				
			||||||
 | 
									err = fmt.Errorf("failed rolling back: %w due to: %w", rbErr, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						if len(current.downQuery) > 0 {
 | 
				
			||||||
 | 
							if _, err := tx.Exec(current.downQuery); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed undoing migration %d: %w", migrationIdx, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return updateSchemaVersion(tx, migrationIdx-1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getSchemaVersion returns the schema version from the database
 | 
				
			||||||
 | 
					func getSchemaVersion(db *sql.DB) int {
 | 
				
			||||||
 | 
						row := db.QueryRowContext(context.Background(), `SELECT version FROM schema_migrations LIMIT 1;`)
 | 
				
			||||||
 | 
						var version int
 | 
				
			||||||
 | 
						if err := row.Scan(&version); err != nil {
 | 
				
			||||||
 | 
							version = -1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return version
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// updateSchemaVersion sets the version to the provided int
 | 
				
			||||||
 | 
					func updateSchemaVersion(tx *sql.Tx, version int) error {
 | 
				
			||||||
 | 
						if version < 0 {
 | 
				
			||||||
 | 
							// Do not try to use the schema_migrations table in a schema version where it doesn't exist
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err := tx.Exec(`UPDATE schema_migrations SET version = @version;`, sql.Named("version", version))
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: Apache-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/sha256"
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// generateAndInsertProjectIDs runs during migration 1, fetches all rows from
 | 
				
			||||||
 | 
					// projects_tmp, loops through the rows generating a repeatable ID for each
 | 
				
			||||||
 | 
					// project, and inserting it into the new table along with the data from the old
 | 
				
			||||||
 | 
					// table.
 | 
				
			||||||
 | 
					func generateAndInsertProjectIDs(tx *sql.Tx) error {
 | 
				
			||||||
 | 
						// Loop through projects_tmp, generate a project_id for each, and insert
 | 
				
			||||||
 | 
						// into projects
 | 
				
			||||||
 | 
						rows, err := tx.Query("SELECT url, name, forge, version, created_at FROM projects_tmp")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to list projects in projects_tmp: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for rows.Next() {
 | 
				
			||||||
 | 
							var (
 | 
				
			||||||
 | 
								url        string
 | 
				
			||||||
 | 
								name       string
 | 
				
			||||||
 | 
								forge      string
 | 
				
			||||||
 | 
								version    string
 | 
				
			||||||
 | 
								created_at string
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err := rows.Scan(&url, &name, &forge, &version, &created_at); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to scan row from projects_tmp: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							id := fmt.Sprintf("%x", sha256.Sum256([]byte(url+name+forge+created_at)))
 | 
				
			||||||
 | 
							_, err = tx.Exec(
 | 
				
			||||||
 | 
								"INSERT INTO projects (id, url, name, forge, version, created_at) VALUES (@id, @url, @name, @forge, @version, @created_at)",
 | 
				
			||||||
 | 
								sql.Named("id", id),
 | 
				
			||||||
 | 
								sql.Named("url", url),
 | 
				
			||||||
 | 
								sql.Named("name", name),
 | 
				
			||||||
 | 
								sql.Named("forge", forge),
 | 
				
			||||||
 | 
								sql.Named("version", version),
 | 
				
			||||||
 | 
								sql.Named("created_at", created_at),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to insert project into projects: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := tx.Exec("DROP TABLE projects_tmp"); err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to drop projects_tmp: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- SPDX-License-Identifier: CC0-1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					--ALTER TABLE projects RENAME TO projects_tmp; -- noqa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE projects RENAME TO projects_tmp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE IF NOT EXISTS projects (
 | 
				
			||||||
 | 
					    url TEXT NOT NULL PRIMARY KEY,
 | 
				
			||||||
 | 
					    name TEXT NOT NULL,
 | 
				
			||||||
 | 
					    forge TEXT NOT NULL,
 | 
				
			||||||
 | 
					    version TEXT NOT NULL,
 | 
				
			||||||
 | 
					    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					INSERT INTO projects (url, name, forge, version, created_at)
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    url,
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    forge,
 | 
				
			||||||
 | 
					    version,
 | 
				
			||||||
 | 
					    created_at
 | 
				
			||||||
 | 
					FROM projects_tmp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DROP TABLE projects_tmp;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 | 
				
			||||||
 | 
					--
 | 
				
			||||||
 | 
					-- SPDX-License-Identifier: CC0-1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE projects RENAME TO projects_tmp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE IF NOT EXISTS projects (
 | 
				
			||||||
 | 
					    id TEXT NOT NULL PRIMARY KEY,
 | 
				
			||||||
 | 
					    url TEXT NOT NULL,
 | 
				
			||||||
 | 
					    name TEXT NOT NULL,
 | 
				
			||||||
 | 
					    forge TEXT NOT NULL,
 | 
				
			||||||
 | 
					    version TEXT NOT NULL,
 | 
				
			||||||
 | 
					    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ type Project struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Release struct {
 | 
					type Release struct {
 | 
				
			||||||
 | 
						ID      string
 | 
				
			||||||
	URL     string
 | 
						URL     string
 | 
				
			||||||
	Tag     string
 | 
						Tag     string
 | 
				
			||||||
	Content string
 | 
						Content string
 | 
				
			||||||
| 
						 | 
					@ -70,6 +71,7 @@ func fetchReleases(dbConn *sql.DB, p Project) (Project, error) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		for _, release := range rssReleases {
 | 
							for _, release := range rssReleases {
 | 
				
			||||||
			p.Releases = append(p.Releases, Release{
 | 
								p.Releases = append(p.Releases, Release{
 | 
				
			||||||
 | 
									ID:      genReleaseID(p.URL, release.URL, release.Tag),
 | 
				
			||||||
				Tag:     release.Tag,
 | 
									Tag:     release.Tag,
 | 
				
			||||||
				Content: release.Content,
 | 
									Content: release.Content,
 | 
				
			||||||
				URL:     release.URL,
 | 
									URL:     release.URL,
 | 
				
			||||||
| 
						 | 
					@ -88,6 +90,7 @@ func fetchReleases(dbConn *sql.DB, p Project) (Project, error) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		for _, release := range gitReleases {
 | 
							for _, release := range gitReleases {
 | 
				
			||||||
			p.Releases = append(p.Releases, Release{
 | 
								p.Releases = append(p.Releases, Release{
 | 
				
			||||||
 | 
									ID:      genReleaseID(p.URL, release.URL, release.Tag),
 | 
				
			||||||
				Tag:     release.Tag,
 | 
									Tag:     release.Tag,
 | 
				
			||||||
				Content: release.Content,
 | 
									Content: release.Content,
 | 
				
			||||||
				URL:     release.URL,
 | 
									URL:     release.URL,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue