4 Commits
v4 ... v5

Author SHA1 Message Date
f8bc389ae5 ci: derive binary version from git describe instead of a hardcoded string
All checks were successful
Release Binaries / build (amd64, golang:1.24-alpine, linux/amd64, linux-amd64, apk add --no-cache upx) (release) Successful in 13s
Release Binaries / build (arm64, golang:1.24-alpine, linux/arm64, linux-arm64, apk add --no-cache upx zstd-static) (release) Successful in 17s
main.go's version var now defaults to "dev" for local builds and is
overridden at build time via -ldflags "-X main.version=...". The
release workflow computes VERSION with `git describe --tags --always
--dirty` (fetch-depth: 0 so tags are available) and injects it into
the Go build inside the container.
2026-07-10 00:14:06 +02:00
72efcb13ba refactor(parser): remove unreachable duplicate priority check in step 5
All checks were successful
Release Binaries / build (amd64, golang:1.24-alpine, linux/amd64, linux-amd64, apk add --no-cache upx) (release) Successful in 13s
Release Binaries / build (arm64, golang:1.24-alpine, linux/arm64, linux-arm64, apk add --no-cache upx zstd-static) (release) Successful in 16s
Step 4 already consumes the (X) priority token before step 5 runs, so
the identical check inside the `!t.Completed` block could never match.
No behavior change (verified against test.todo.txt).
2026-07-09 23:57:32 +02:00
096216579d fix(parser): warn instead of silently dropping tasks with invalid due dates
A malformed due: value (e.g. due:2024-13-99) left DueDate nil, causing
ToRemind to silently emit nothing for that task. Now ParseLine reports
an error so the problem surfaces on stderr instead of vanishing.

Also fixes staticcheck QF1012 (WriteString(Sprintf(...)) -> Fprintf)
flagged by the IDE in ToRemind.
2026-07-09 23:55:18 +02:00
d692fa5a4c ci: add Gitea release workflow for AMD64/ARM64 binaries
All checks were successful
Release Binaries / build (amd64, golang:1.24-alpine, linux/amd64, linux-amd64, apk add --no-cache upx) (release) Successful in 35s
Release Binaries / build (arm64, golang:1.24-alpine, linux/arm64, linux-arm64, apk add --no-cache upx zstd-static) (release) Successful in 44s
Builds the Go binary on dedicated amd64/arm64 runners via Docker and
uploads both artifacts to the Gitea release.
2026-07-09 23:33:46 +02:00
4 changed files with 98 additions and 19 deletions

View File

@@ -0,0 +1,74 @@
name: Release Binaries
on:
release:
types: [published]
jobs:
build:
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
image: golang:1.24-alpine
runner: linux-amd64
upx_install: "apk add --no-cache upx"
- arch: arm64
platform: linux/arm64
image: golang:1.24-alpine
runner: linux-arm64
upx_install: "apk add --no-cache upx zstd-static"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build (${{ matrix.arch }})
run: |
set -o pipefail
VERSION="$(git describe --tags --always --dirty)"
echo "Version: $VERSION"
# Crea il container senza avviarlo
CID=$(docker create \
--platform ${{ matrix.platform }} \
-w /src \
${{ matrix.image }} \
sleep infinity)
# Copia i sorgenti dentro con docker cp (funziona anche in DinD)
docker cp "${{ github.workspace }}/." "$CID:/src"
# Avvia ed esegui la build
docker start "$CID"
docker exec "$CID" sh -c "
${{ matrix.upx_install }}
export CGO_ENABLED=0
go build -trimpath -ldflags='-s -w -X main.version=$VERSION' -o /src/dist/todotxt2remind .
upx /src/dist/todotxt2remind
" 2>&1 | cat
# Copia il binario fuori
docker cp "$CID:/src/dist/todotxt2remind" \
"${{ github.workspace }}/todotxt2remind-linux-${{ matrix.arch }}"
docker rm -f "$CID"
- name: Carica artefatto sulla release
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ github.event.release.id }}
run: |
FILENAME="todotxt2remind-linux-${{ matrix.arch }}"
curl -s -X POST \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${RELEASE_ID}/assets?name=${FILENAME}" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${FILENAME}"

View File

@@ -44,10 +44,10 @@ func (t Task) ToRemind(indent int) string {
// k must have the first letter uppercase for Remind // k must have the first letter uppercase for Remind
kUpper := strings.ToUpper(k[:1]) + k[1:] kUpper := strings.ToUpper(k[:1]) + k[1:]
if len(values) == 1 { if len(values) == 1 {
sb.WriteString(fmt.Sprintf("INFO \"%s: %s\" ", kUpper, values[0])) fmt.Fprintf(&sb, "INFO \"%s: %s\" ", kUpper, values[0])
} else { } else {
for i, v := range values { for i, v := range values {
sb.WriteString(fmt.Sprintf("INFO \"%s%d: %s\" ", kUpper, i+1, v)) fmt.Fprintf(&sb, "INFO \"%s%d: %s\" ", kUpper, i+1, v)
} }
} }
} }
@@ -55,22 +55,22 @@ func (t Task) ToRemind(indent int) string {
if len(t.Projects) > 0 { if len(t.Projects) > 0 {
for _, p := range t.Projects { for _, p := range t.Projects {
sb.WriteString(fmt.Sprintf("INFO \"List: %s\" ", p)) fmt.Fprintf(&sb, "INFO \"List: %s\" ", p)
} }
} }
if len(t.Contexts) > 0 { if len(t.Contexts) > 0 {
for _, c := range t.Contexts { for _, c := range t.Contexts {
sb.WriteString(fmt.Sprintf("INFO \"Tag: %s\" ", c)) fmt.Fprintf(&sb, "INFO \"Tag: %s\" ", c)
} }
} }
if t.Completed { if t.Completed {
sb.WriteString(fmt.Sprintf("COMPLETE-THROUGH %s ", t.DueDate.Format("2006-01-02"))) fmt.Fprintf(&sb, "COMPLETE-THROUGH %s ", t.DueDate.Format("2006-01-02"))
} }
if t.Priority != nil { if t.Priority != nil {
sb.WriteString(fmt.Sprintf("PRIORITY %d ", t.PriorityAsRemind())) fmt.Fprintf(&sb, "PRIORITY %d ", t.PriorityAsRemind())
} }
sb.WriteString("MSG") sb.WriteString("MSG")
@@ -79,7 +79,7 @@ func (t Task) ToRemind(indent int) string {
sb.WriteString(" %<List>:") sb.WriteString(" %<List>:")
} }
sb.WriteString(fmt.Sprintf(" %s", t.Description)) fmt.Fprintf(&sb, " %s", t.Description)
if t.Completed { if t.Completed {
sb.WriteString("%:") sb.WriteString("%:")
@@ -97,11 +97,11 @@ func (t Task) ToRemind(indent int) string {
// uppercase first letter for Remind // uppercase first letter for Remind
kUpper := strings.ToUpper(k[:1]) + k[1:] kUpper := strings.ToUpper(k[:1]) + k[1:]
if len(values) == 1 { if len(values) == 1 {
sb.WriteString(fmt.Sprintf("%%_%s%s: %%<%s>", headingSpaces, kUpper, kUpper)) fmt.Fprintf(&sb, "%%_%s%s: %%<%s>", headingSpaces, kUpper, kUpper)
} else { } else {
for i := range values { for i := range values {
kNumbered := fmt.Sprintf("%s%d", kUpper, i+1) kNumbered := fmt.Sprintf("%s%d", kUpper, i+1)
sb.WriteString(fmt.Sprintf("%%_%s%s: %%<%s>", headingSpaces, kNumbered, kNumbered)) fmt.Fprintf(&sb, "%%_%s%s: %%<%s>", headingSpaces, kNumbered, kNumbered)
} }
} }
} }
@@ -202,6 +202,7 @@ func ParseLine(line string) (Task, error) {
working := strings.TrimSpace(line) working := strings.TrimSpace(line)
toks := splitTokens(working) toks := splitTokens(working)
i := 0 i := 0
var invalidDue string
// 1) Completed? // 1) Completed?
if i < len(toks) && toks[i] == "x" { if i < len(toks) && toks[i] == "x" {
@@ -230,14 +231,8 @@ func ParseLine(line string) (Task, error) {
i++ i++
} }
// 5) If not completed, check for priority at start // 5) Creation date for incomplete tasks (priority, if any, was already consumed in step 4)
if !t.Completed { if !t.Completed {
if i < len(toks) && len(toks[i]) == 3 && toks[i][0] == '(' && toks[i][2] == ')' && toks[i][1] >= 'A' && toks[i][1] <= 'Z' {
r := rune(toks[i][1])
t.Priority = &r
i++
}
// Creation date for incomplete tasks
if i < len(toks) && dateRe.MatchString(toks[i]) { if i < len(toks) && dateRe.MatchString(toks[i]) {
if dt, err := time.Parse(dateLayout, toks[i]); err == nil { if dt, err := time.Parse(dateLayout, toks[i]); err == nil {
t.CreationDate = &dt t.CreationDate = &dt
@@ -285,9 +280,12 @@ func ParseLine(line string) (Task, error) {
k, v := parts[0], parts[1] k, v := parts[0], parts[1]
if k != "" && v != "" && !isProtocolKey(k) { if k != "" && v != "" && !isProtocolKey(k) {
t.Metadata[k] = append(t.Metadata[k], v) t.Metadata[k] = append(t.Metadata[k], v)
if k == "due" && dateRe.MatchString(v) { if k == "due" {
if dt, err := time.Parse(dateLayout, v); err == nil { dt, err := time.Parse(dateLayout, v)
if dateRe.MatchString(v) && err == nil {
t.DueDate = &dt t.DueDate = &dt
} else {
invalidDue = v
} }
} }
continue continue
@@ -302,6 +300,10 @@ func ParseLine(line string) (Task, error) {
return t, errors.New("completed task missing completion date (spec requires completion date directly after 'x')") return t, errors.New("completed task missing completion date (spec requires completion date directly after 'x')")
} }
if invalidDue != "" {
return t, fmt.Errorf("invalid due date %q: expected format YYYY-MM-DD (task will be dropped, no Remind entry generated)", invalidDue)
}
return t, nil return t, nil
} }

View File

@@ -15,7 +15,7 @@ var (
outputFile string outputFile string
debug bool debug bool
indent int indent int
version = "v3.0.0" version = "dev" // overridden at build time via -ldflags "-X main.version=..."
) )
func main() { func main() {

View File

@@ -73,6 +73,9 @@ X 2012-01-01 Make resolutions
(A) 2024-13-99 Call Mom (A) 2024-13-99 Call Mom
x 2024-99-99 Call Mom x 2024-99-99 Call Mom
# Malformed: due date with invalid format (should error, task dropped from output)
(A) Call Mom due:2024-13-99
# Malformed: key:value with whitespace in key or value # Malformed: key:value with whitespace in key or value
foo bar:baz foo bar:baz
foo:bar baz foo:bar baz